Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

High memory usage on random file reads using seek_read (Windows) #122473

Closed
nazar-pc opened this issue Mar 14, 2024 · 28 comments
Closed

High memory usage on random file reads using seek_read (Windows) #122473

nazar-pc opened this issue Mar 14, 2024 · 28 comments
Labels
C-discussion Category: Discussion or questions that doesn't represent real issues. O-windows Operating system: Windows

Comments

@nazar-pc
Copy link

Here is a small app:

//! ```cargo
//! [dependencies]
//! libc = "0.2.151"
//! rand = "0.8.5"
//!
//! [target.'cfg(windows)'.dependencies]
//! winapi = { version = "0.3.9", features = ["winbase"] }
//! ```

use rand::prelude::*;
use std::fs::{File, OpenOptions};
use std::io::{Seek, SeekFrom};
use std::{env, io};

const SECTOR_SIZE: usize = 1024 * 1024 * 1024;
const CHUNK_SIZE: usize = 32;

fn main() -> io::Result<()> {
    let file = env::args().nth(1).unwrap();

    let sectors = (File::open(&file)?.seek(SeekFrom::End(0))? / SECTOR_SIZE as u64) as usize;

    let file = OpenOptions::new()
        .read(true)
        .advise_random_access()
        .open(&file)?;
    file.advise_random_access()?;

    let mut result = vec![[0u8; CHUNK_SIZE]; sectors];

    for i in 0.. {
        (0..sectors)
            .into_iter()
            .zip(&mut result)
            .try_for_each(|(offset, result)| {
                let sector_offset = offset * SECTOR_SIZE;
                let offset_within_sector =
                    thread_rng().gen_range(0..SECTOR_SIZE / CHUNK_SIZE) * CHUNK_SIZE;

                file.read_at(result, sector_offset + offset_within_sector)
            })?;

        if i > 0 && i % 10_000 == 0 {
            println!("{i} iterations");
        }
    }

    Ok(())
}

trait ReadAtSync: Send + Sync {
    /// Fill the buffer by reading bytes at a specific offset
    fn read_at(&self, buf: &mut [u8], offset: usize) -> io::Result<()>;
}

impl ReadAtSync for File {
    fn read_at(&self, buf: &mut [u8], offset: usize) -> io::Result<()> {
        self.read_exact_at(buf, offset as u64)
    }
}

/// Extension convenience trait that allows setting some file opening options in cross-platform way
trait OpenOptionsExt {
    /// Advise OS/file system that file will use random access and read-ahead behavior is
    /// undesirable, only has impact on Windows, for other operating systems see [`FileExt`]
    fn advise_random_access(&mut self) -> &mut Self;
}

impl OpenOptionsExt for OpenOptions {
    #[cfg(target_os = "linux")]
    fn advise_random_access(&mut self) -> &mut Self {
        // Not supported
        self
    }

    #[cfg(target_os = "macos")]
    fn advise_random_access(&mut self) -> &mut Self {
        // Not supported
        self
    }

    #[cfg(windows)]
    fn advise_random_access(&mut self) -> &mut Self {
        use std::os::windows::fs::OpenOptionsExt;
        self.custom_flags(winapi::um::winbase::FILE_FLAG_RANDOM_ACCESS)
    }
}

/// Extension convenience trait that allows pre-allocating files, suggesting random access pattern
/// and doing cross-platform exact reads/writes
trait FileExt {
    /// Advise OS/file system that file will use random access and read-ahead behavior is
    /// undesirable, on Windows this can only be set when file is opened, see [`OpenOptionsExt`]
    fn advise_random_access(&self) -> io::Result<()>;

    /// Read exact number of bytes at a specific offset
    fn read_exact_at(&self, buf: &mut [u8], offset: u64) -> io::Result<()>;
}

impl FileExt for File {
    #[cfg(target_os = "linux")]
    fn advise_random_access(&self) -> io::Result<()> {
        use std::os::unix::io::AsRawFd;
        let err = unsafe { libc::posix_fadvise(self.as_raw_fd(), 0, 0, libc::POSIX_FADV_RANDOM) };
        if err != 0 {
            Err(std::io::Error::from_raw_os_error(err))
        } else {
            Ok(())
        }
    }

    #[cfg(target_os = "macos")]
    fn advise_random_access(&self) -> io::Result<()> {
        use std::os::unix::io::AsRawFd;
        if unsafe { libc::fcntl(self.as_raw_fd(), libc::F_RDAHEAD, 0) } != 0 {
            Err(std::io::Error::last_os_error())
        } else {
            Ok(())
        }
    }

    #[cfg(windows)]
    fn advise_random_access(&self) -> io::Result<()> {
        // Not supported
        Ok(())
    }

    #[cfg(unix)]
    fn read_exact_at(&self, buf: &mut [u8], offset: u64) -> io::Result<()> {
        std::os::unix::fs::FileExt::read_exact_at(self, buf, offset)
    }

    #[cfg(windows)]
    fn read_exact_at(&self, mut buf: &mut [u8], mut offset: u64) -> io::Result<()> {
        while !buf.is_empty() {
            match std::os::windows::fs::FileExt::seek_read(self, buf, offset) {
                Ok(0) => {
                    break;
                }
                Ok(n) => {
                    buf = &mut buf[n..];
                    offset += n as u64;
                }
                Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {
                    // Try again
                }
                Err(e) => {
                    return Err(e);
                }
            }
        }

        if !buf.is_empty() {
            Err(std::io::Error::new(
                std::io::ErrorKind::UnexpectedEof,
                "failed to fill whole buffer",
            ))
        } else {
            Ok(())
        }
    }
}

What it does is reading small chunks of a large file at random in a loop.

It can be stored in file.rs and then used as cargo +nightly -Zscript file.rs D:\large-file.bin, where large-file.bin in my case is 500G.

I expected to see this happen:

Windows is saying application uses 0.5MiB of RAM:

Знімок екрана з 2024-02-27 05-44-47

Instead, this happened:

However, total Windows memory usage in a minute or two grows by ~1GiB (sometimes less, sometimes more) and I can't find where it goes. Amount depends on file size. This looks like either bug in Windows or something fishy happening in Rust standard library, but either way seems to be outside of my knowledge to find out.

Screenshot of task manager while application is running and after application is stopped:

Знімок екрана з 2024-02-27 05-48-21
Знімок екрана з 2024-02-27 05-48-59

I later discovered that it is used by memory mapping apparently created somewhere within Windows kernel because it doesn't belong to any apps and I definitely didn't request memory mapped file:

Знімок екрана з 2024-03-01 02-19-19

The example above is cross-platform and doesn't have such issues on Linux. Happens on both Windows 10 and Windows 11.

This is a reduced example of a real-world app where a single app sometimes manages tens of terabytes of large files. Appreciate any hints.

Meta

rustc --version --verbose:

rustc 1.78.0-nightly (c475e2303 2024-02-28)
binary: rustc
commit-hash: c475e2303b551d726307c646181e0677af1e0069
commit-date: 2024-02-28
host: x86_64-pc-windows-msvc
release: 1.78.0-nightly
LLVM version: 18.1.0

Initially reported at https://internals.rust-lang.org/t/high-memory-usage-on-random-file-reads-using-seek-read-windows/20396?u=nazar-pc (with some discussion), then at https://answers.microsoft.com/en-us/windows/forum/windows_10-performance/windows-is-leaking-memory-when-reading-random/965c422c-cb71-4dbb-9d3e-49dfa7b9df76, was redirected to https://learn.microsoft.com/en-us/answers/questions/1601862/windows-is-leaking-memory-when-reading-random-chun (there is useful debugging info there), then redirected to https://techcommunity.microsoft.com/t5/microsoft-learn/windows-is-leaking-memory-when-reading-random-chunks-of-a-large/m-p/4074012 by Microsoft support and finally in response to twitter post @kennykerr suggested to report it here, so here I am 🤞

@nazar-pc nazar-pc added the C-bug Category: This is a bug. label Mar 14, 2024
@rustbot rustbot added the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label Mar 14, 2024
@ChrisDenton
Copy link
Member

Rust cannot control how the Windows kernel uses and manages its file cache, other than by completely opting out of caching. Which wouldn't be a desirable default.

@nazar-pc
Copy link
Author

nazar-pc commented Mar 14, 2024

I agree, but it was recommended to post it here anyway 🤷 : https://twitter.com/ClintRutkas/status/1768052358955827405

Disabling buffering completely with FILE_FLAG_NO_BUFFERING helps, but it has its own issues and is awkward to work with too.

@Noratrieb Noratrieb added O-windows Operating system: Windows C-discussion Category: Discussion or questions that doesn't represent real issues. T-libs Relevant to the library team, which will review and decide on the PR/issue. and removed C-bug Category: This is a bug. needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. labels Mar 14, 2024
@kennykerr
Copy link
Contributor

I heard from a colleague about a memory leak involving the Rust Standard Library and suggested an issue be opened with a minimal repro. This does not however appear to be a memory leak, as you've already discovered. If you need more control over the behavior of file reads you may want to experiment with using the Windows API directly. I will just say that Windows may buffer or cache file I/O intentionally, often involving memory mapped files, to improve performance based on available memory.

@nazar-pc
Copy link
Author

But I clearly provided a hint to OS that I do random reads in a huge file. There isn't anything OS can possibly cache to improve performance by definition of random reads, hence bug report. Linux and macOS do not have this issue and Windows should disable contents caching when such hint is provided as well.

@kennykerr
Copy link
Contributor

If you think there's a bug in the caching behavior, please provide a minimal repro using just the Windows API and ideally in C. I don't want to make extra work for you but the OS engineers who can investigate generally don't have much experience with Rust and that should make it easier for them to quickly pinpoint an issue or provide some guidance. I'll also ping some Windows/Rust folks directly in case they have some other suggestions:

cc @arlosi @sivadeilra @dpaoliello

@saethlin
Copy link
Member

Is there an actual problem here? From the kernel's perspective, free memory is wasted memory. So seeing your free memory go to 0 is only a problem if the kernel then fails to discard cache to serve memory allocation requests. Does it?

@the8472
Copy link
Member

the8472 commented Mar 14, 2024

From the linked conversations it sounds like the memory isn't properly freed under pressure.

This is MUCH worse when even larger files are involved, similar app easily makes Windows use over 100G of RAM for no good reason. For some users this means active swapping to disk and sluggish experience, some just run out of memory and crash.

This kind of degradation is important to include in the issue since it's different from reclaimable memory not being reclaimed instantly.

@ChrisDenton
Copy link
Member

ChrisDenton commented Mar 14, 2024

Hm, I missed that. If memory isn't being reclaimed when needed then that's more serious than I initially realised.

Still, if it's happening in the kernel then I'm not sure Rust can do much about it.

@nazar-pc
Copy link
Author

From the kernel's perspective, free memory is wasted memory

Yes, but:

  • in this case it is CPU that is being wasted because caching is explicitly pointless
  • it is actively used memory that is being occupied, not cache; as the result even if it is being reclaimed, it is not fast enough, so from user's point of view they run my app, open Task Manager and see ~100% memory usage until they close the app, things are sluggish due to swapping to disk and some things are just crashing due to lack of memory; so as far as user is concerned it is my fault and app is bad

I'll rewrite and share demo with windows crate later. I don't write C if I have a choice to not do so, so it'll be still in Rust.

@sivadeilra
Copy link

Without more info, this sounds like the normal, expected behavior of the Windows kernel, specifically the buffer manager. The buffer manager caches filesystem data, regardless of what means is used to access the filesystem data. In other words, it will cache data in the kernel whether you're reading using ReadFile (equivalent of read(2)) or memory-mapped I/O. The buffer manager will evict pages from this cache on-demand, and the caching is done at the OS scope, so it is expected that the cached data will stay in memory even after the process terminates.

Are you seeing any actual degradation in performance, or are you basing this problem report on counters in Task Manager? If it's just the counter, then this is generally not a problem, because using memory for caching is the expected behavior of the Windows kernel. That memory will be reclaimed, on demand.

seek_read itself does not allocate private process memory. All it does is call ReadFile and pass a non-null OVERLAPPED* pointer, which contains the file offset for the read operation.

The reason that this is the expected behavior is that the kernel is trying to provide "general" good service for apps. Random reads from files is expected, normal service, especially for executable files (which are memory-mapped, with pages being touched on-demand). All mainstream operating systems cache pieces of files.

If caching is harmful for your application, then you should disable cached reads when opening the file. The documentation for CreateFileW describes the file flags that can be used when opening the file. The FILE_FLAG_NO_BUFFERING flag indicates that an app does not want the kernel to do any buffering on the file. The kernel will still query the buffer cache on reads, since FILE_FLAG_NO_BUFFERING only applies to the specific file handle that resulted from a specific call to CreateFileW; other apps may have opened the same file, so this flag does not disable the cache entirely. Instead, the flag indicates that file accesses for this handle will not insert new data into the buffer cache.

Random reads from very large files, with very little data locality, are exactly the reason that the FILE_FLAG_NO_BUFFERING flag exists, so this would be appropriate. You will need to call CreateFileW directly, however, because I don't know of a way to pass this flag from Rust.

@ChrisDenton
Copy link
Member

You will need to call CreateFileW directly, however, because I don't know of a way to pass this flag from Rust.

custom_flags. Though I believe the OP is aware of this.

@nazar-pc
Copy link
Author

@sivadeilra I'm not sure you noticed FILE_FLAG_RANDOM_ACCESS in demo app, but it is crucial.

Regardless whether it is expected for Windows to cache things when FILE_FLAG_RANDOM_ACCESS is specified right now, I consider it a bug that needs to be fixed. So far everyone seems to point out that this is expected behavior on Windows (here and in other linked threads), but it doesn't necessarily mean it should stay this way.

No other operating systems I deal with (Linux and macOS) cache contents of files when provided random access hints because it makes no sense, so I expect very similar behavior from Windows.

Working with FILE_FLAG_NO_BUFFERING means re-architecting an app to deal with alignment requirements and reimplementing a bunch of logic that is already present in the kernel.
I did use FILE_FLAG_NO_BUFFERING as a workaround already and hit a few other issues (I have no idea where to report them so they actually reach someone knowledgeable, please recommend while I have your attention), but I hope to get rid of it eventually when FILE_FLAG_RANDOM_ACCESS is respected.

@the8472
Copy link
Member

the8472 commented Mar 14, 2024

as the result even if it is being reclaimed, it is not fast enough [...] things are sluggish due to swapping to disk and some things are just crashing due to lack of memory

Imo this is the crucial aspect for prioritization. You should provide more details about that rather than discussing whether/how much cache should or shouldn't be used.

@sivadeilra
Copy link

Access patterns and cacheability are independent aspects of an app's behavior, which is why FILE_FLAG_RANDOM_ACCESS and FILE_FLAG_NO_BUFFERING are different flags. For example, apps that repeatedly read the contents of a file sequentially also benefit from disabling caching. And there are legitimate reasons for an app to use random access and to use caching. FILE_FLAG_RANDOM_ACCESS is a hint that normal optimizations like read-ahead should not be used.

Regardless, this is clearly not a Rust-specific issue.

@ChrisDenton
Copy link
Member

I'm closing this issue on that basis. I have no objection to people continuing to discuss this here but there's nothing for Rust to track.

@ChrisDenton ChrisDenton closed this as not planned Won't fix, can't repro, duplicate, stale Mar 14, 2024
@nazar-pc
Copy link
Author

And there are legitimate reasons for an app to use random access and to use caching

I disagree strongly and Linux/macOS developers apparently too, but I'm willing to help however I can to resolve whatever you believe the root issue is here. FILE_FLAG_NO_BUFFERING to me is more similar to direct I/O on Linux, meaning it is a completely different thing from caching, but does imply no caching too. Cache though is typically disabled with just a random access hint.

Regardless, this is clearly not a Rust-specific issue.

Agree, where should the discussion continue? I posted this in 7 places already and this is the only place that attracted attention of relevant (I think) people. I can do email or whatever else you folks prefer.

@workingjubilee
Copy link
Member

I disagree strongly and Linux/macOS developers apparently too,

I have no idea why I am being involved in this conversation, I don't agree with you at all.

@nazar-pc
Copy link
Author

nazar-pc commented Mar 14, 2024

@kennykerr, assuming I didn't mess up too much, here is the same app with windows crate directly:

//! ```cargo
//! [dependencies]
//! rand = "0.8.5"
//! windows = { version = "0.54.0", features = ["Wdk_Storage_FileSystem", "Win32_Storage_FileSystem", "Win32_System_IO", "Win32_Security", "Win32_System_Threading"] }
//! ```

use rand::prelude::*;
use std::env;
use std::fs::File;
use std::io::{Seek, SeekFrom};
use windows::core::PCWSTR;
use windows::Wdk::Storage::FileSystem::NtReadFile;
use windows::Win32::Foundation::{GENERIC_ACCESS_RIGHTS, GENERIC_READ, HANDLE, STATUS_PENDING};
use windows::Win32::Storage::FileSystem::FILE_FLAG_RANDOM_ACCESS;
use windows::Win32::Storage::FileSystem::{
    CreateFileW, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
};
use windows::Win32::System::Threading::{WaitForSingleObject, INFINITE};
use windows::Win32::System::IO::IO_STATUS_BLOCK;

const SECTOR_SIZE: usize = 1024 * 1024 * 1024;
const CHUNK_SIZE: usize = 32;

fn main() {
    let file = env::args().nth(1).unwrap();

    // Just to measure size
    let sectors =
        (File::open(&file).unwrap().seek(SeekFrom::End(0)).unwrap() / SECTOR_SIZE as u64) as usize;

    let mut path = file.encode_utf16().collect::<Vec<_>>();
    path.push(0);

    let file = unsafe {
        let GENERIC_ACCESS_RIGHTS(access) = GENERIC_READ;
        CreateFileW(
            PCWSTR::from_raw(path.as_ptr()),
            access,
            FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
            None,
            OPEN_EXISTING,
            FILE_FLAG_RANDOM_ACCESS,
            HANDLE::default(),
        )
        .unwrap()
    };

    let mut result = [0u8; CHUNK_SIZE];
    let mut io_status = IO_STATUS_BLOCK::default();

    for i in 0.. {
        for offset in 0..sectors {
            let sector_offset = offset * SECTOR_SIZE;
            let offset_within_sector =
                thread_rng().gen_range(0..SECTOR_SIZE / CHUNK_SIZE) * CHUNK_SIZE;

            unsafe {
                let byteoffset = (sector_offset + offset_within_sector) as i64;

                let status = NtReadFile(
                    file,
                    HANDLE::default(),
                    None,
                    None,
                    &mut io_status,
                    result.as_mut_ptr() as *mut _,
                    CHUNK_SIZE as u32,
                    Some(&byteoffset),
                    None,
                );

                if status == STATUS_PENDING {
                    WaitForSingleObject(file, INFINITE);
                }
            }

            if i > 0 && i % 1000 == 0 {
                println!("{i} iterations");
            }
        }
    }
}

With large enough files this eventually results in ~100% memory usage as seeing in task manager (in real app I have this operation happening in multiple threads, so memory usage is growing quickly).

@jieyouxu jieyouxu removed the T-libs Relevant to the library team, which will review and decide on the PR/issue. label Mar 15, 2024
@kennykerr
Copy link
Contributor

I ran your example against a 500G file for about an hour. CPU and memory usage remained stable. I was able to continue working with no system degradation. I'm running a very recent build of Windows 11, Intel i9-12900K processor, and 64GB of RAM.

@nazar-pc
Copy link
Author

I ran your example against a 500G file for about an hour. CPU and memory usage remained stable. I was able to continue working with no system degradation. I'm running a very recent build of Windows 11, Intel i9-12900K processor, and 64GB of RAM.

That is expected and matches what I see as well. Memory usage grows within about a minute to some level and settles after that. The amount of memory used depends on file size, to consume all 64G you will need to have multiple TB worth of files. I have users with 16T+ of SSDs on one machine and they struggle even with 64G of memory on Windows.

The insight here is that you will see that process uses something like 0.5M of memory, while actively used system memory grows by a lot more than that and stays there until application is closed.

@nazar-pc
Copy link
Author

Here is what I see with 921G file while application is running and after it was stopped:

Знімок екрана з 2024-03-15 16-38-52
Знімок екрана з 2024-03-15 16-39-14

It shows that ~7.7G of memory was used by the app (it typically seems to approach 100%, but not quite reach it).
As you can imagine having 16T + memory used by other application logic barely fits into 128G.

When users see such picture in Task Manager they become really worried that they need to buy more RAM. I understand it is some sort of caching, but it doesn't look right no matter how I look at it.

@workingjubilee
Copy link
Member

If there is no actual performance concern with the Windows API, as used by Rust or otherwise, and this is instead about people not being able to interpret the graphs that the Task Manager emits, then this is a feature request to the Task Manager team and strictly out of bounds of the auspices of this venue.

@nazar-pc
Copy link
Author

If there is no actual performance concern with the Windows API, as used by Rust or otherwise, and this is instead about people not being able to interpret the graphs that the Task Manager emits, then this is a feature request to the Task Manager team and strictly out of bounds of the auspices of this venue.

I agree, but in this case Task Manager is correctly showing what is happening in the kernel: memory is being actively used by something. And this high memory usage does cause performance concerns and even crashes in some cases as mentioned before.

@workingjubilee
Copy link
Member

If you have crash reproduction instructions, then please provide them.

@Atales
Copy link

Atales commented Apr 16, 2024

Some history and original intent behind FILE_FLAG_RANDOM_ACCESS, it did 2 things when it comes to Cache-Manager(Cc's) behavior:

  1. First, access being random, Cc disables read-ahead
    Because, by very nature read-pattern is random and therefore impossible to predict which file offset to pre-fetch with good enough confidence that it will be useful to the app, Cc disables reading ahead on these files
  2. The second thing Cc does is disable unmapping of file from the system cache
    Predicated on the premise that access is random and as long as there's no memory pressure, we can keep the file in cache until it is closed, which increases the chances of cache-hit on some future cached-read.
    This second bit (of never unmapping until the file is closed) is the one that caused serious cache-bloat issues, so we did away with that logic (around 2016), and now we unmap FILE_FLAG_RANDOM_ACCESS files just like we do for other files without this flag.
    But this unmap, while better than nothing, is not the most ideal; it is good-enough and easy-enough to prevent issues in the most common usecases. And that is what we observed as well, all the customers complaining about cache bloat when using FILE_FLAG_RANDOM_ACCESS were satisfied.

(Note: The way unmap works is, at the time of a cached read, for a given file offset, Cc peeks a few pages forward and backwards and only unmaps those adjoining pages. Now, because read pattern is random, the unmap is random as well and for a large enough file with long enough random access, we can see how it can keep dodging unmap logic just enough to consume a lot of memory. However, at some point Memory Manger (Mm) should detect the on-going/imminent memory pressure and Mm should try to evict pages from memory (system cache as well as other processes' working sets) to deal with the increased memory demand. Maybe the test app runs fast enough to beat Mm's memory-pressure sensing/reclaim speed (this part about beating Mm is purely a speculation on my part!).

Just because a file is accessed in random fashion doesn't mean it can never benefit from it being cached in the system, the cache hit-rate will be random (and mostly lower than that of a sequentially access file), but hopefully never zero; and the hit-rate cost-benefit would greatly depend on the workload.

In the Windows parlance, cached read without caching doesn't make much sense, that's what Non-Cached (unbuffered) read is (CACHING in this context is analogous to BUFFERING). However, I DO see your point of not wanting to change the app to do non-cached reads due to alignment requirements for non-cached/unbuffered reads.
What you really want is not FILE_FLAG_RANDOM_ACCESS, but something new (that doesn't exist today) that tells the system to do Cached Reads but NOT cache the data, perhaps, FILE_FLAG_CACHED_READ_EVICT_CACHE which, apparently, is what Linux and MacOS is doing with the RANDOM flag at the moment.

While a reasonable request to expect Linux/MacOS -like behavior from Windows, it is not a complaint we have heard enough to consider investing in changing it at this moment. There's no right or wrong behavior for this particular flag, it just so happens that, at the time, Windows chose a meaning/implementation that made most sense for their ecosystem, which is different from Linux/MacOS. And Windows has to be careful about changing its behavior under the hood, lest we break some workload that might explicitly or implicitly depend on caching for RANDOM files.

Hope this provides some context on "by-design" behavior that you are experiencing.

@nazar-pc : As to issues with unbuffered reads, please start a new thread and we'll investigate that as well.

@nazar-pc
Copy link
Author

Just because a file is accessed in random fashion doesn't mean it can never benefit from it being cached in the system, the cache hit-rate will be random (and mostly lower than that of a sequentially access file), but hopefully never zero; and the hit-rate cost-benefit would greatly depend on the workload.

What you really want is not FILE_FLAG_RANDOM_ACCESS, but something new (that doesn't exist today) that tells the system to do Cached Reads but NOT cache the data, perhaps, FILE_FLAG_CACHED_READ_EVICT_CACHE which, apparently, is what Linux and MacOS is doing with the RANDOM flag at the moment.

Might be, but my original expectation (before knowing more than I wanted about these things) was that FILE_FLAG_RANDOM_ACCESS means access is truly random, which by extension means cache hits are approaching 0%, so if you are getting anything higher than that you're just lucky and wasting CPU/memory doing caching anyway. My intuition was clearly incorrect.

I reported this same issue with different wording in https://aka.ms/AApjksl

While a reasonable request to expect Linux/MacOS -like behavior from Windows, it is not a complaint we have heard enough to consider investing in changing it at this moment. There's no right or wrong behavior for this particular flag, it just so happens that, at the time, Windows chose a meaning/implementation that made most sense for their ecosystem, which is different from Linux/MacOS. And Windows has to be careful about changing its behavior under the hood, lest we break some workload that might explicitly or implicitly depend on caching for RANDOM files.

Makes sense, but every response online I saw so far was basically "it is fine, just use unbuffered", which is quite painful to do in cross-platform apps. Also I don't think outside databases (which there aren't too many of out there) there are many apps that are so read-intensive as an app I'm working on to really show case how bad it is, especially with truly random reads on huge files. If you can consider implementing something like this, that'd make my life much easier next time I face a similar requirement.

@nazar-pc : As to issues with unbuffered reads, please start a new thread and we'll investigate that as well.

Reported on Feedback Hub with link to inimal reproduction with Windows APIs at https://aka.ms/AApjlev, thanks! It is an edge-case, but still.

@beck-8
Copy link

beck-8 commented Jun 17, 2024

image
So for a program like this, no matter how many pages of files I add, it’s only a matter of time before it runs out of memory?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-discussion Category: Discussion or questions that doesn't represent real issues. O-windows Operating system: Windows
Projects
None yet
Development

No branches or pull requests