📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

๐Ÿ“š Interview question

What is the difference between sequential and random file access? Why does it matter for performance on HDDs and SSDs, and which .NET APIs (FileOptions, RandomAccess, memory-mapped files) help with each?

Sequential vs random I/O, device and OS effects (seek time, read-ahead, page cache), FileOptions hints, the RandomAccess API, and memory-mapped files.

Senior Theory Advanced file-system io memory-mapped-files performance random-access ssd

๐ŸŽฏ Key points

  • Sequential access benefits from read-ahead and contiguous device transfers
  • Random access pays a positioning cost per request (HDD ms, SSD tens of µs)
  • Cloud disks cap IOPS and throughput; small random I/O hits IOPS caps
  • FileOptions.SequentialScan / RandomAccess are OS caching hints
  • RandomAccess.Read/Write use explicit offsets on a SafeFileHandle, thread-safe, no shared Position
  • Memory-mapped files let the OS page data in; first touch is a hidden page fault
  • Turn random writes into sequential ones: append, batch, sort by offset

โฑ 30-second answer

  • Sequential access reads or writes bytes in order (log files, CSV imports, backups). The OS reads ahead, the device streams contiguous blocks, and throughput is at its highest.
  • Random access jumps to arbitrary offsets (database pages, indexes, lookups in a big binary file). Each request pays a positioning cost: ~5โ€“10 ms seek on an HDD, ~10โ€“100 ยตs on an SSD.

In .NET: use a normal FileStream (optionally FileOptions.SequentialScan and a bigger buffer) for sequential work. For random work use RandomAccess.Read/Write with a SafeFileHandle (offset-based, thread-safe, no shared Position), FileOptions.RandomAccess, or a MemoryMappedFile when you want the OS to page data in for you.

๐Ÿ—ฃ Full answer (senior)

Why the pattern matters

Device Sequential throughput Random 4 KiB reads
HDD (7,200 rpm) ~150โ€“250 MB/s ~100โ€“200 IOPS (โ‰ˆ0.5โ€“1 MB/s)
SATA SSD ~500 MB/s ~50โ€“90k IOPS
NVMe SSD 3โ€“14 GB/s hundreds of thousands of IOPS or more
Cloud disk Capped by tier (MB/s) Capped by tier (IOPS)

On an HDD random access can be hundreds of times slower than sequential. On SSDs the gap is smaller but still real, and cloud disks bill and throttle by IOPS, so many small random reads hit limits quickly.

OS effects

  • Read-ahead: when the kernel sees sequential reads it fetches the next blocks before you ask.
  • Page cache: repeated random reads of hot regions come from RAM.
  • FileOptions.SequentialScan / RandomAccess are hints (posix_fadvise on Linux, cache manager flags on Windows) to tune read-ahead and caching.

.NET APIs

Need API
Stream through a file once FileStream / StreamReader, 64 KiB+ buffer, FileOptions.SequentialScan
Read records at known offsets, from many threads File.OpenHandle + RandomAccess.Read(handle, buffer, offset) (.NET 6+)
Treat a large file like memory, OS-managed paging MemoryMappedFile.CreateFromFile + CreateViewAccessor / CreateViewStream
Get the file length cheaply from a handle RandomAccess.GetLength(handle)

Production example

A pricing service looked up product records in a 40 GB fixed-width binary file on a cloud disk. It used one shared FileStream with Seek + Read under a lock, so requests were serialised and p99 was 300 ms. We switched to RandomAccess.ReadAsync(handle, buffer, offset) without a lock (offset-based reads are thread-safe), opened the file with FileOptions.RandomAccess | FileOptions.Asynchronous, and moved to a disk tier with more IOPS. p99 dropped to 15 ms. Hot records were then served from the page cache, which we sized the VM for.

๐Ÿ” What the interviewer is checking

Checks systems thinking about I/O performance and knowledge of modern .NET file APIs.

Green flags

  • Explains seek cost, read-ahead and page cache.
  • Knows SSD and cloud disk nuances (IOPS caps, queue depth).
  • Knows RandomAccess and why a shared Position is a problem.
  • Weighs memory-mapped files honestly.

Red flags

  • "SSDs make it irrelevant."
  • Uses Seek on one stream from many threads without understanding the race.

๐Ÿ“š Learn in depth

1. What the device does

HDD: random read = seek arm + wait for platter rotation + transfer
      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
      โ”‚  ~4 ms seek + ~4 ms rotation + 0.02 ms data   โ”‚   per random 4 KiB
      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
     sequential = one seek, then a continuous stream

SSD: no moving parts, but
  - reads/writes happen in pages (4โ€“16 KiB), erases in large blocks
  - parallelism across channels needs many outstanding requests (queue depth)
  - small random WRITES cause write amplification

2. Why one FileStream is a bottleneck for random reads

FileStream has a single Position. Two threads doing Seek then Read race each other, so you need a lock, which serialises all I/O.

Thread A: Seek(1000) โ”€โ”€โ”            โ”Œโ”€โ”€ Read  โ† reads at 5000! โœ˜
Thread B:        Seek(5000) โ”€โ”€ Read โ”€โ”˜

RandomAccess.Read(handle, buffer, offset) maps to pread/ReadFile with an explicit offset. No shared position, so many threads can read in parallel, which also gives SSDs the queue depth they need.

3. Memory-mapped files

process virtual memory
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ view of data.bin       โ”‚โ”€โ”€ page fault on first touch โ”€โ”€โ–บ OS reads 4 KiB page from disk
โ”‚ (looks like an array)  โ”‚โ”€โ”€ later touches โ”€โ”€โ–บ straight from page cache, no system call
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Pros: no system call per read once cached, the OS shares pages across processes, simple code for read-mostly data. Cons: page faults are hidden I/O (unpredictable latency on a cold cache), I/O errors surface as exceptions on memory access, and very large views need 64-bit address space.

4. Designing for sequential I/O

Many systems turn random writes into sequential ones on purpose:

  • Databases write a sequential write-ahead log, then update pages later.
  • LSM-tree stores (RocksDB, Cassandra) append to files and merge in the background.
  • Kafka is an append-only log read sequentially by consumers.

For your own files: sort work by offset, batch small reads into larger ones, and append instead of updating in place.

5. Try it yourself

  1. Create a 4 GB file. Time reading it start to end with a 1 MiB buffer.
  2. Time 100,000 random 4 KiB reads at random offsets with RandomAccess.Read.
  3. Repeat both after dropping the page cache (sync; echo 3 | sudo tee /proc/sys/vm/drop_caches on a test Linux box) to see cold vs warm.
  4. Run the random test with 1 thread and with 16 threads (Parallel.ForAsync) and compare throughput on an SSD.

๐Ÿ’ป Code

// Random, parallel, offset-based reads of fixed-size records (.NET 6+)
public sealed class RecordFile : IDisposable
{
    private const int RecordSize = 256;
    private readonly SafeFileHandle _handle;

    public RecordFile(string path) =>
        _handle = File.OpenHandle(path, FileMode.Open, FileAccess.Read, FileShare.Read,
            FileOptions.RandomAccess | FileOptions.Asynchronous);

    public long Count => RandomAccess.GetLength(_handle) / RecordSize;

    public async ValueTask<byte[]> ReadAsync(long index, CancellationToken ct)
    {
        var buffer = new byte[RecordSize];
        int read = await RandomAccess.ReadAsync(_handle, buffer, index * RecordSize, ct);
        if (read != RecordSize) throw new EndOfStreamException();
        return buffer;  // thread-safe: no shared Position, no lock needed
    }

    public void Dispose() => _handle.Dispose();
}
// Sequential scan with a larger buffer and a read-ahead hint
await using var fs = new FileStream("/data/events.log", new FileStreamOptions
{
    Mode = FileMode.Open,
    Access = FileAccess.Read,
    Share = FileShare.Read,
    BufferSize = 1024 * 1024,
    Options = FileOptions.SequentialScan | FileOptions.Asynchronous
});
using var reader = new StreamReader(fs);
while (await reader.ReadLineAsync(ct) is { } line) Handle(line);
// Memory-mapped lookup in a read-mostly index
using var mmf = MemoryMappedFile.CreateFromFile("/data/index.bin", FileMode.Open, null, 0,
    MemoryMappedFileAccess.Read);
using var view = mmf.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read);
long offset = 1_234_567L * 16;
long key = view.ReadInt64(offset);
long value = view.ReadInt64(offset + 8);

โš  Common mistakes

  • Mistake: Sharing one FileStream across threads for random reads with Seek + Read. The single Position causes races, so people add a lock and serialise all I/O. Instead: use RandomAccess.Read/ReadAsync with an explicit offset on a SafeFileHandle.
  • Mistake: Assuming SSDs make access patterns irrelevant. Random small I/O still costs more per byte, needs queue depth for throughput, and hits IOPS caps on cloud disks. Instead: batch, sort by offset and measure against the disk tier's IOPS and MB/s limits.
  • Mistake: Benchmarking with a warm page cache. The second run reads from RAM and looks amazing. Instead: test cold and warm, and size RAM for the hot set if you rely on caching.
  • Mistake: Using memory-mapped files and ignoring page faults. Each first touch is a disk read hidden inside a memory access, so latency is unpredictable. Instead: use them for read-mostly data that fits in cache, or pre-warm, and monitor major faults.
  • Mistake: Updating records in place at random positions in large files when appends would work. Random writes are the slowest pattern and cause write amplification on SSDs. Instead: append and compact later, or use a database designed for it.

๐Ÿ› Senior insight: trade-offs

Access pattern often matters more than code speed. I first ask whether the workload is sequential, random, or can be made sequential (sort, batch, append-only). Databases, Kafka and LSM stores all win by turning random writes into sequential ones, and the same trick works for application files.

For genuinely random reads, the modern .NET answer is RandomAccess on a SafeFileHandle: offset-based, lock-free across threads, and able to keep an SSD's queue busy. Memory-mapped files are great for read-mostly data that fits in the page cache, but they hide I/O inside page faults, which I monitor. And in the cloud, I check the disk tier's IOPS and throughput caps before blaming code; many "slow file" incidents are simply throttled disks.

๐Ÿ”— Follow-ups the interviewer may ask

  1. When: Candidate mentions memory-mapped files paging data in Why: Checks page fault understanding
    How to answer

    A page fault is a CPU exception raised when code touches a virtual address whose page is not currently mapped to physical RAM for that process. The kernel handles it:

    • Soft (minor) fault: the page is already in RAM (file cache, shared library, a freshly zeroed page, copy-on-write) and only the page table needs updating. Costs microseconds.
    • Hard (major) fault: the page must be read from disk (swap file, page file or a memory-mapped file). Costs milliseconds, around 1,000 times slower.
    • Invalid access: no mapping at all, so the process gets an access violation / SIGSEGV.

    Minor faults are normal. A burst of major faults means the working set does not fit in RAM, and latency spikes follow.

  2. When: Candidate talks about locality Why: Connects disk access patterns to CPU cache locality
    How to answer

    CPUs load memory in cache lines of 64 bytes, so after one access the neighbouring bytes are already in cache (spatial locality), and recently used data is likely still there (temporal locality). Hardware prefetchers also spot sequential access and fetch ahead. A C# int[,] is stored row-major: one row's elements are next to each other. Walking row by row uses every byte of each line and the prefetcher keeps up; walking column by column jumps a whole row each step, wasting most of each line and missing cache, often 5โ€“10ร— slower for large arrays. The same logic favours arrays of structs over lists of objects scattered on the heap, and struct-of-arrays layouts for hot numeric fields.

  3. When: Candidate mentions processing large files Why: Applies access patterns to a real import design
    How to answer

    Never load the file. Stream it: open with a large buffer and SequentialScan, parse line by line with a fast CSV parser (or PipeReader + UTF-8 parsing), and push rows into a bounded Channel so the reader cannot outrun the writers. Insert in batches (e.g. 10,000โ€“50,000 rows) with SqlBulkCopy into a staging table, then merge into the real table in one transaction.

    Make it resumable (checkpoint byte offset or line number per committed batch), send bad rows to a reject file with line numbers, make the load idempotent (file hash, staging swap), and emit progress metrics. Memory stays at a few hundred MB regardless of file size.

๐Ÿ›  Real-world problems: how you handle them

Serialised random reads behind a lock on a 40 GB lookup file
Situation
[Pricing service] on .NET [10] looking up product records in a [40 GB] fixed-width file on a [Premium SSD] cloud disk at [2,000] requests/s.
Signals
p99 latency [300 ms], CPU low, thread pool queue growing, disk IOPS well below the tier cap.
Investigation
A trace showed requests waiting on a `lock` around `Seek` + `Read` on one shared `FileStream`. Disk metrics showed queue depth of about 1, so the SSD was idle most of the time.
Resolution
Replaced with `RandomAccess.ReadAsync` on a `SafeFileHandle` with `FileOptions.RandomAccess`, removed the lock, and raised the disk tier for headroom.
Prevention
Benchmarks for cold and warm cache in CI, dashboards for disk IOPS and queue depth next to latency.

How to tell it (STAR)

**S:** Our [pricing service] had [300 ms] p99 even though CPU and disk looked quiet. **T:** I had to get it under [50 ms] before [a campaign launch]. **A:** Tracing showed every lookup waiting on a lock around one shared `FileStream`, so the SSD only ever had one read in flight. I switched to `RandomAccess.ReadAsync` with explicit offsets, removed the lock and checked the disk tier limits. **R:** p99 fell to [15 ms] and throughput rose [8x] on the same hardware.

โ†ฉ Usually asked after

More in File System

Last updated 23 Oct 2026.

๐ŸŒ Sangam's Webring

โช Previous ๐Ÿ  Main Next โฉ
๐Ÿ’ฌ Chat with me