对文件/目录设置时间戳非常缓慢

4
我正在开发一个项目,需要复制许多文件和目录,同时保留它们的原始时间戳。因此,我需要多次调用目标的SetCreationTime()SetLastWriteTime()SetLastAccessTime()方法,以便将源中的原始值复制到目标中。如下面的屏幕截图所示,这些简单操作占用了总计算时间的 42%。 performance analysis 由于这极大地限制了我的整个应用程序的性能,我想加快速度。我猜测,每个调用都需要打开和关闭与文件/目录的新流。如果是这个原因,我想在写完所有属性之前保持这个流打开。我该怎么做?我猜这需要使用一些 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);
        }
}

我使用的枚举类型可以在这里找到。这样只需要打开文件一次就能完成所有操作:创建文件、应用所有属性、设置时间戳并从原始文件复制实际内容。
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%。

优化前的结果:

before

优化后的结果:

after


1
请参考http://referencesource.microsoft.com/#mscorlib/system/io/file.cs,63bd669b43be5f17以查看“File.SetCreationTimeUtc”的实现。您可能需要编写一个函数,该函数P/Invokes“CreateFile”,然后调用“SetFileTime”。 - Jim Mischel
1个回答

7
我不是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;

我注意到设置文件时间中昂贵的部分是打开/关闭文件,大约60%的时间用于打开文件,大约40%用于关闭文件(需要将修改刷新到磁盘)。以上循环对10000次迭代花费了约9秒。
一些研究表明,使用FILE_WRITE_ATTRIBUTES(而不是GENERIC_WRITE)调用CreateFile(..)足以更改文件的时间属性。
这种修改显着加快了事情的速度!现在相同的循环在10000次迭代中完成2秒。由于迭代次数相当小,我进行了第二次运行,共有100000个迭代,以获取更可靠的时间测量:
- FILE_WRITE_ATTRIBUTES:100,000次迭代的5次运行:12.7-13.2秒 - GENERIC_WRITE:100,000次迭代的5次运行:63.2-72.5秒
根据上述数字,我的猜测是C#方法在打开文件以更改文件时间时使用了错误的访问模式。或者其他C#行为会减慢事情...
也许解决你速度问题的方法是实现一个导出C函数的DLL,使用SetFileTime(..)更改文件时间? 或者甚至可以直接导入CreateFile(..)SetFileTime(..)CloseHandle(..)函数,避免调用C#方法?

祝你好运!


哇,谢谢你提供这么详细的答案!我会尝试使用FILE_WRITE_ATTRIBUTES,这应该可以解决我的问题。 - wodzu
不需要编写一个 DLL 来实现这个功能。你可以编写一个调用 Windows API 函数的 C# 函数。 - Jim Mischel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接