Disable cacheing before reading from a file using Swift

Hello,

I am trying to implement some sort of minimal I/O benchmarking feature. I can calculate the write speeds with the following code:

guard var handle = FileHandle(forUpdatingAtPath: url.path) else { return }

var writtenData: UInt64 = 0
var totalWrittenData: UInt64 = 0
var writeSpeed: UInt64 = 0

let data = Data(repeating: 0, count: 16 * 1024 * 1024) // 16 MB test block
let startTime = Date.now.timeIntervalSince1970

while Date.now.timeIntervalSince1970 - startTime < 5.0
{
    handle.write(data)
    try? handle.synchronize()self.totalWrittenData += blockSize

    let duration = Date.timestamp - self.startTime
    writeSpeed = UInt64(Double(self.totalWrittenData) / duration)

    if writtenData > blockSize * 16
    {
         writtenDara = 0
         try? handle.seek(toOffset: 0)
    }
}

// On my PCE-Express SSD this results in roughly 1800MB/s. This value matches the value reported by other benchmarking apps.

// Remove everything at the end of the file
try? handle.truncate(atOffset: blockSize)

try? handle.synchronize()
try? handle.seek(toOffset: 0)

var index = 0
while index < 5
{
     autoreleasepool
    {
        var startTime = Date.timestamp
        let bytes = try? handle.readToEnd()
        let duration = Date.timestamp - startTime

         Swift.print(bytes?.count, duration)
         try? handle.seek(toOffset: 0)
     }

     index += 1
}

The code works - in theory. Although I really like that the operating system and drivers do some cacheing to optimise performance - in my use case I don't want files to be cached. The read results are always around 8GB/s for my PCE-Express SSD and also for some older USB Sticks, so the data seems to come directly from the memory.

I found a couple of other threads which led me to some older C functions, specifically this code:

// Open file.
let fd = fopen(fileUrl.path, "r")

// Find the end
fseek(fd, 0, SEEK_END)

// Count bytes
let fileByteSize = ftell(fd)

// Return to start
fseek(fd2, 0, SEEK_SET)

// Disable cache
setvbuf(fd2, nil, _IONBF, 0)

// Find the end
fseek(fd2, 0, SEEK_END)

let readBytes = fread(pointer, 1, fileByteSize, fd2)

readBytes += UInt64(readBytes.count)

When executing this code (or parts of it), it also works fine - but the data is also coming from the cache.

What am I missing? I also tried the FileManager. Of course I am not an expert, however I think that the FileManager is "just" a wrapper around all those C functions.

Is there any way to ignore the cacheing mechanism and tell macOS / the driver to read the data directly from the drive?

Regards, Sascha

Accepted Reply

The droid you’re looking for here is F_NOCACHE, as documented in the fcntl man page.

If you’re using this to profile I/O performance, make sure that:

  • Your memory buffer is page aligned.

  • The offset in the file is a multiple of the page size.

  • The length is a multiple of the page size.

Also, if you’re doing a write test, pre-allocate your storage using F_PREALLOCATE.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Replies

The droid you’re looking for here is F_NOCACHE, as documented in the fcntl man page.

If you’re using this to profile I/O performance, make sure that:

  • Your memory buffer is page aligned.

  • The offset in the file is a multiple of the page size.

  • The length is a multiple of the page size.

Also, if you’re doing a write test, pre-allocate your storage using F_PREALLOCATE.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hello,

thanks for the reply. I have updated my while loop to use the following logic:

var buffer = Data(count: 4 * 1024 * 1024)
        
let fd = open(fileUrl.path, O_RDONLY)
if fcntl(fd, F_NOCACHE, 1) == -1 { return }

while Date.now.timeIntervalSince1970 - startTime < 5.0
{
    buffer.withUnsafeMutableBytes
    {
        ptr in
                    
        let amount = read(fd, ptr.baseAddress, ptr.count)

        self.readData += UInt64(amount)
        self.totalReadData += UInt64(amount)
    }
            
    lseek(fd, 0, SEEK_SET)
}

close(fd)

The calculated speed goes up to almost 16GB/s. I tried moving the open and close calls directly into the while loop. Although this slows down the logic (as one would expect), the speed is still roughly 8GB/s.

Regards, Sascha

Hello,

my problem was that I only set the F_NOCACHE flag when reading the file. At this point, the file is already in the cache. The key is to also set this flag when writing to the file. This seems to no cache the file.

Thanks again.

Regards, Sascha