UNC-путь, указывающий на локальный каталог намного медленнее, чем локальный доступ

некоторый код, с которым я работаю, иногда должен ссылаться на длинные UNC-пути (например,\?UNCMachineNamePath), но мы обнаружили, что независимо от того, где находится каталог, даже на том же компьютере, он намного медленнее при доступе через UNC-путь, чем локальный путь.

например, мы написали некоторый код бенчмаркинга, который записывает строку тарабарщины в файл, а затем перечитывает ее несколько раз. Я тестирую его с 6 различными способами доступа к одному и тому же общий каталог на моей машине dev, с кодом, запущенным на той же машине:

  • C:Temp
  • MachineNameTemp
  • \?C:Temp
  • \?UNCMachineNameTemp
  • \127.0.0.1Temp
  • \?UNC127.0.0.1Temp

и вот результаты:

Testing: C:Temp
Wrote 1000 files to C:Temp in 861.0647 ms
Read 1000 files from C:Temp in 60.0744 ms
Testing: MachineNameTemp
Wrote 1000 files to MachineNameTemp in 2270.2051 ms
Read 1000 files from MachineNameTemp in 1655.0815 ms
Testing: ?C:Temp
Wrote 1000 files to ?C:Temp in 916.0596 ms
Read 1000 files from ?C:Temp in 60.0517 ms
Testing: ?UNCMachineNameTemp
Wrote 1000 files to ?UNCMachineNameTemp in 2499.3235 ms
Read 1000 files from ?UNCMachineNameTemp in 1684.2291 ms
Testing: 127.0.0.1Temp
Wrote 1000 files to 127.0.0.1Temp in 2516.2847 ms
Read 1000 files from 127.0.0.1Temp in 1721.1925 ms
Testing: ?UNC7.0.0.1Temp
Wrote 1000 files to ?UNC7.0.0.1Temp in 2499.3211 ms
Read 1000 files from ?UNC7.0.0.1Temp in 1678.18 ms

я попробовал IP-адрес, чтобы исключить проблему DNS. Может ли это быть проверка учетных данных или разрешений для каждого доступа к файлам? Если да, то есть способ спрятать его? Он просто предполагает, поскольку это путь UNC, что он должен делать все через TCP/IP вместо прямого доступа к диску? Это что-то не так с кодом, который мы используем для чтения/записи? Я вырвал соответствующие части для бенчмаркинга, см. ниже:

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;
using Util.FileSystem;

namespace UNCWriteTest {
    internal class Program {
        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern bool DeleteFile(string path); // File.Delete doesn't handle ?UNC paths

        private const int N = 1000;

        private const string TextToSerialize =
            "asd;lgviajsmfopajwf0923p84jtmpq93worjgfq0394jktp9orgjawefuogahejngfmliqwegfnailsjdhfmasodfhnasjldgifvsdkuhjsmdofasldhjfasolfgiasngouahfmp9284jfqp92384fhjwp90c8jkp04jk34pofj4eo9aWIUEgjaoswdfg8jmp409c8jmwoeifulhnjq34lotgfhnq34g";

        private static readonly byte[] _Buffer = Encoding.UTF8.GetBytes(TextToSerialize);

        public static string WriteFile(string basedir) {
            string fileName = Path.Combine(basedir, string.Format("{0}.tmp", Guid.NewGuid()));

            try {
                IntPtr writeHandle = NativeFileHandler.CreateFile(
                    fileName,
                    NativeFileHandler.EFileAccess.GenericWrite,
                    NativeFileHandler.EFileShare.None,
                    IntPtr.Zero,
                    NativeFileHandler.ECreationDisposition.New,
                    NativeFileHandler.EFileAttributes.Normal,
                    IntPtr.Zero);

                // if file was locked
                int fileError = Marshal.GetLastWin32Error();
                if ((fileError == 32 /* ERROR_SHARING_VIOLATION */) || (fileError == 80 /* ERROR_FILE_EXISTS */)) {
                    throw new Exception("oopsy");
                }

                using (var h = new SafeFileHandle(writeHandle, true)) {
                    using (var fs = new FileStream(h, FileAccess.Write, NativeFileHandler.DiskPageSize)) {
                        fs.Write(_Buffer, 0, _Buffer.Length);
                    }
                }
            }
            catch (IOException) {
                throw;
            }
            catch (Exception ex) {
                throw new InvalidOperationException(" code " + Marshal.GetLastWin32Error(), ex);
            }

            return fileName;
        }

        public static void ReadFile(string fileName) {
            var fileHandle =
                new SafeFileHandle(
                    NativeFileHandler.CreateFile(fileName, NativeFileHandler.EFileAccess.GenericRead, NativeFileHandler.EFileShare.Read, IntPtr.Zero,
                                                 NativeFileHandler.ECreationDisposition.OpenExisting, NativeFileHandler.EFileAttributes.Normal, IntPtr.Zero), true);

            using (fileHandle) {
                //check the handle here to get a bit cleaner exception semantics
                if (fileHandle.IsInvalid) {
                    //ms-help://MS.MSSDK.1033/MS.WinSDK.1033/debug/base/system_error_codes__0-499_.htm
                    int errorCode = Marshal.GetLastWin32Error();
                    //now that we've taken more than our allotted share of time, throw the exception
                    throw new IOException(string.Format("file read failed on {0} to {1} with error code {1}", fileName, errorCode));
                }

                //we have a valid handle and can actually read a stream, exceptions from serialization bubble out
                using (var fs = new FileStream(fileHandle, FileAccess.Read, 1*NativeFileHandler.DiskPageSize)) {
                    //if serialization fails, we'll just let the normal serialization exception flow out
                    var foo = new byte[256];
                    fs.Read(foo, 0, 256);
                }
            }
        }

        public static string[] TestWrites(string baseDir) {
            try {
                var fileNames = new List<string>();
                DateTime start = DateTime.UtcNow;
                for (int i = 0; i < N; i++) {
                    fileNames.Add(WriteFile(baseDir));
                }
                DateTime end = DateTime.UtcNow;

                Console.Out.WriteLine("Wrote {0} files to {1} in {2} ms", N, baseDir, end.Subtract(start).TotalMilliseconds);
                return fileNames.ToArray();
            }
            catch (Exception e) {
                Console.Out.WriteLine("Failed to write for " + baseDir + " Exception: " + e.Message);
                return new string[] {};
            }
        }

        public static void TestReads(string baseDir, string[] fileNames) {
            try {
                DateTime start = DateTime.UtcNow;

                for (int i = 0; i < N; i++) {
                    ReadFile(fileNames[i%fileNames.Length]);
                }
                DateTime end = DateTime.UtcNow;

                Console.Out.WriteLine("Read {0} files from {1} in {2} ms", N, baseDir, end.Subtract(start).TotalMilliseconds);
            }
            catch (Exception e) {
                Console.Out.WriteLine("Failed to read for " + baseDir + " Exception: " + e.Message);
            }
        }

        private static void Main(string[] args) {
            foreach (string baseDir in args) {
                Console.Out.WriteLine("Testing: {0}", baseDir);

                string[] fileNames = TestWrites(baseDir);

                TestReads(baseDir, fileNames);

                foreach (string fileName in fileNames) {
                    DeleteFile(fileName);
                }
            }
        }
    }
}

1 ответов


Это меня не удивляет. Вы пишете / читаете довольно небольшой объем данных, поэтому кэш файловой системы, вероятно, минимизирует влияние ввода-вывода на физический диск; в основном узким местом будет CPU. Я не уверен, будет ли трафик проходить через стек TCP/IP или нет, но, как минимум, протокол SMB участвует. Во-первых, это означает, что запросы передаются между клиентским процессом SMB и серверным процессом SMB, поэтому у вас есть переключение контекста между тремя различными процессами, включая ваш собственный. Используя путь локальной файловой системы, вы переключаетесь в режим ядра и обратно, но никакой другой процесс не задействован. Переключение контекста много медленнее, чем переход в режим ядра и из него.

вероятно, будут две отдельные дополнительные накладные расходы: одна на файл и одна на килобайт данных. В этом конкретном тесте накладные расходы SMB на файл, вероятно, будут доминирующими. Потому что количество данные, участвующие также влияет на влияние физического диска ввода - вывода, вы можете обнаружить, что это только действительно проблема при работе с большим количеством небольших файлов.