Hidden cost of GetFileInformationByHandle
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:
- An application that iterates over thousands of files in various locations, querying their timestamps
- A service that frequently reads a small number of “hot” files and queries their sizes in advance to preallocate buffers
- 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:
| Field | Information class |
|---|---|
DWORD dwFileAttributes | Basic file information |
FILETIME ftCreationTime | Basic file information |
FILETIME ftLastAccessTime | Basic file information |
FILETIME ftLastWriteTime | Basic file information |
DWORD dwVolumeSerialNumber | Volume information |
DWORD nFileSizeHigh | Standard file information |
DWORD nFileSizeLow | Standard file information |
DWORD nNumberOfLinks | Standard file information |
DWORD nFileIndexHigh | Internal file information |
DWORD nFileIndexLow | Internal 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
FileAllInformationclass that was used to obtain all three necessary information categories (basic, standard and internal file information) in a single call toNtQueryFileInformation. -
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:
GetFileInformationByHandleExwith a specific information class, orGetFileSizeExwhich is a convenient wrapper if you only need the file size.
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.