Skip to main content
Back to the blog

Hidden cost of GetFileInformationByHandle

· 5 min read

The documentation describes the GetFileInformationByHandle Windows API function as a “more basic version” of its advanced variant. While it offers simplicity and ease of use, that convenience comes at a cost.

The scenario

It is difficult to find a practical scenario where a single I/O call becomes the actual bottleneck of an entire operation. But for narrative purposes, it could be any of the following:

  1. An application that iterates over thousands of files in various locations, querying their timestamps
  2. A service that frequently reads a small number of “hot” files and queries their sizes in advance to preallocate buffers
  3. A developer so obsessed with the I/O efficiency of a function that they consciously ignore the fact it only runs once a week¹

Then, at some point you discover that the runtime library you are using is calling GetFileInformationByHandle to answer those queries.

Maybe the library is relatively old and didn’t know any better at the time, or maybe it decided to transparently map the result of the API call. Regardless, this has doubled the number of required system calls, and forced them to retrieve more information than necessary.

How exactly did that happen?

Legacy of GetFileInformationByHandle

The GetFileInformationByHandle populates a BY_HANDLE_FILE_INFORMATION structure with various information about the file:

struct BY_HANDLE_FILE_INFORMATION {
DWORD dwFileAttributes;
FILETIME ftCreationTime;
FILETIME ftLastAccessTime;
FILETIME ftLastWriteTime;
DWORD dwVolumeSerialNumber;
DWORD nFileSizeHigh;
DWORD nFileSizeLow;
DWORD nNumberOfLinks;
DWORD nFileIndexHigh;
DWORD nFileIndexLow;
};

However, this structure is just a user-space facade. What we’re actually interested in is how this maps to the kernel representation of the same information, i.e., to the NT system calls.

From the structure definition, you might have noticed that the returned information falls into different categories. And from the standpoint of the NT API, the fields map to the following information classes:

FieldInformation class
DWORD dwFileAttributesBasic file information
FILETIME ftCreationTimeBasic file information
FILETIME ftLastAccessTimeBasic file information
FILETIME ftLastWriteTimeBasic file information
DWORD dwVolumeSerialNumberVolume information
DWORD nFileSizeHighStandard file information
DWORD nFileSizeLowStandard file information
DWORD nNumberOfLinksStandard file information
DWORD nFileIndexHighInternal file information
DWORD nFileIndexLowInternal file information

Given that there are four different information classes, does filling the structure require four different syscalls? Let’s find that out.

Going practical

Below is a quick snippet we can use to compare how those different calls behave, assuming we’re only interested in basic file information:

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>

HANDLE file = CreateFile(
L"test",
FILE_ALL_ACCESS,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL,
OPEN_ALWAYS,
0,
NULL);

FILE_BASIC_INFO basicInfo = {0};
GetFileInformationByHandleEx(
file,
FileBasicInfo,
&basicInfo,
sizeof(basicInfo));

BY_HANDLE_FILE_INFORMATION byHandleInfo = {0};
GetFileInformationByHandle(file, &byHandleInfo);

Here is how this program behaves in Process Monitor:

So, while we didn’t get four separate syscalls, GetFileInformationByHandle still performed two syscalls instead of one:

  • There is a special FileAllInformation class that was used to obtain all three necessary information categories (basic, standard and internal file information) in a single call to NtQueryFileInformation.

  • But volume information is fully separated from file information, so the function also had to call NtQueryVolumeInformationFile.

Assuming our goal was simply to get the basic file metadata such as its timestamp, that extra call is just wasted CPU cycles and additional context switch overhead.

Connection with POSIX?

While the way this API works can be explained by entirely legacy reasons, sometimes there are deeper connections to be found.

In this case, the BY_HANDLE_FILE_INFORMATION structure looks remarkably similar to POSIX struct stat, both containing the device (volume) ID, number of links, and inode (index) numbers:

struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* Inode number */
mode_t st_mode; /* File type and mode */
nlink_t st_nlink; /* Number of hard links */
uid_t st_uid; /* User ID of owner */
gid_t st_gid; /* Group ID of owner */
dev_t st_rdev; /* Device ID (if special file) */
off_t st_size; /* Total size, in bytes */
blksize_t st_blksize; /* Block size for filesystem I/O */
blkcnt_t st_blocks; /* Number of 512 B blocks allocated */

struct timespec st_atim; /* Time of last access */
struct timespec st_mtim; /* Time of last modification */
struct timespec st_ctim; /* Time of last status change */
};

The conclusion: be specific

This inefficiency can be fixed by using an API that maps with the underlying kernel primitive, requesting only the data you actually intend to consume:

By using the more specific APIs, you can cut the syscalls in half and remove the overhead of querying volume information.

Finally, beware of the odd-looking QueryInformationVolume Process Monitor entries for files. While they may not contribute to the overall performance of your application, they can indicate you’re inadvertently using the more costly version of the API.


¹ This case has happened more times than we care to admit.