在 files/directories 上设置时间戳非常慢

Setting Timestamps on files/directories is extremely slow

我正在做一个项目,它需要复制大量文件和目录,同时保留它们的原始时间戳。因此,我需要多次调用目标的 SetCreationTime()SetLastWriteTime()SetLastAccessTime() 方法,以便将原始值从源复制到目标。正如下面的屏幕截图所示,这些简单的操作占用了总计算时间的 42%。

由于这极大地限制了我的整个应用程序的性能,我想加快速度。我假设,这些调用中的每一个都需要打开和关闭 file/directory 的新流。如果是这个原因,我想让这个流保持打开状态,直到我完成所有属性的编写。我该如何做到这一点?我想这需要使用一些 P/Invoke.

更新:

我按照 Lukas 的建议 将 WinAPI 方法 CreateFile(..)FILE_WRITE_ATTRIBUTES 一起使用。为了 P/Invoke 提到的方法,我创建了以下包装器:

public class Win32ApiWrapper
{
    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    private static extern SafeFileHandle CreateFile(string lpFileName,
                                                    [MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess,
                                                    [MarshalAs(UnmanagedType.U4)] FileShare dwShareMode,
                                                    IntPtr lpSecurityAttributes, 
                                                    [MarshalAs(UnmanagedType.U4)] FileMode dwCreationDisposition,
                                                    [MarshalAs(UnmanagedType.U4)] FileAttributes dwFlagsAndAttributes,
                                                    IntPtr hTemplateFile);

    public static SafeFileHandle CreateFileGetHandle(string path, int fileAttributes)
    {
        return CreateFile(path,
                (FileAccess)(EFileAccess.FILE_WRITE_ATTRIBUTES | EFileAccess.FILE_WRITE_DATA),
                0,
                IntPtr.Zero,
                FileMode.Create,
                (FileAttributes)fileAttributes,
                IntPtr.Zero);
        }
}

可以找到我使用的枚举 here。这让我只需打开文件一次即可完成所有操作:创建文件、应用所有属性、设置时间戳并从中复制实际内容原始文件。

FileInfo targetFile;
int fileAttributes;
IDictionary<string, long> timeStamps; 

using (var hFile = Win32ApiWrapper.CreateFileGetHandle(targetFile.FullName, attributeFlags))
using (var targetStream = new FileStream(hFile, FileAccess.Write))
{
    // copy file
    Win32ApiWrapper.SetFileTime(hFile, timeStamps);
}

值得付出努力吗?是的。它将计算时间从 86 秒减少到 51 秒,减少了约 40%。

优化前的结果:

优化后的结果:

我不是 C# 程序员,我不知道那些 System.IO.FileSystemInfo 方法是如何实现的。但是我已经用 WIN32 API 函数 SetFileTime(..) 做了一些测试,它会在某些时候被 C# 调用。

这是我的基准测试循环的代码片段:

#define NO_OF_ITERATIONS   100000

int iteration;
DWORD tStart;
SYSTEMTIME tSys;
FILETIME tFile;
HANDLE hFile;
DWORD tEllapsed;


iteration = NO_OF_ITERATIONS;
GetLocalTime(&tSys);
tStart = GetTickCount();
while (iteration)
{
   tSys.wYear++;
   if (tSys.wYear > 2020)
   {
      tSys.wYear = 2000;
   }

   SystemTimeToFileTime(&tSys, &tFile);
   hFile = CreateFile("test.dat",
                      GENERIC_WRITE,   // FILE_WRITE_ATTRIBUTES
                      0,
                      NULL,
                      OPEN_EXISTING,
                      FILE_ATTRIBUTE_NORMAL,
                      NULL);
   if (hFile == INVALID_HANDLE_VALUE)
   {
      printf("CreateFile(..) failed (error: %d)\n", GetLastError());
      break;
   }

   SetFileTime(hFile, &tFile, &tFile, &tFile);

   CloseHandle(hFile);
   iteration--;
}
tEllapsed = GetTickCount() - tStart;

我已经看到设置文件时间的昂贵部分是文件的 opening/closing。大约 60% 的时间用于打开文件,大约 40% 的时间用于关闭文件(这需要将修改刷新到光盘)。上面的循环 10000 次迭代花费了大约 9s。

一项小研究表明,使用 FILE_WRITE_ATTRIBUTES(而不是 GENERIC_WRITE)调用 CreateFile(..) 足以更改文件的时间属性。

此修改显着加快了速度!现在相同的循环在 2 秒内完成 10000 次迭代。由于迭代次数非常少,我用 100000 次迭代进行了第二次 运行 以获得更可靠的时间测量:

  • FILE_WRITE_ATTRIBUTES:5 运行s 100000 次迭代:12.7-13.2s
  • GENERIC_WRITE:5 运行s 100000 次迭代:63.2-72.5s

根据以上数字,我猜测是 C# 方法在打开文件时使用了错误的访问模式以更改为文件时间。或者其他一些 C# 行为会减慢速度...

那么也许解决您的速度问题的方法是实现一个导出 C 函数的 DLL,该函数使用 SetFileTime(..) 更改文件时间?或者您甚至可以直接导入函数 CreateFile(..)SetFileTime(..)CloseHandle(..) 以避免调用 C# 方法?

祝你好运!