В настоящее время я пытаюсь найти самый быстрый способ анализа XML-файла, структура которого хорошо известна (в основном данные игры, которые будут загружены инструментом). Я знаю разметку полностью, вплоть до того, сколько байтов данных я могу пропустить, чтобы получить следующее содержимое тега, и этот файл никогда не снабжается данными, предоставленными пользователем, поэтому вместо использования каких-либо (более медленных) классов, предназначенных для XML, таких как XmlReader и XElement, я просто выполняю синтаксический анализ необработанного указателя, перемещая заданное количество байтов вперед и либо проверяя тег (если это необязательно для данного элемента), либо просто считывая данные напрямую. Моя конечная цель — загрузить данные (список структур) как можно быстрее, не обращая внимания на любые опасения по поводу безопасности ввода и потенциальных осложнений в случае редактирования файла.
Моим следующим предположением о том, как сделать это еще быстрее, было, естественно, распараллелить код, анализируя несколько его частей одновременно. Поскольку ни один XML-элемент не опирался на предыдущий, я мог безопасно анализировать равное (или почти равное) количество элементов в каждом потоке, а затем объединять коллекции.
И хотя я знаю, что существуют определенные накладные расходы на запуск потоков (а в моем случае также на разделение работы на потоки), я все же ожидал, что параллельная версия превзойдет по производительности последовательную версию на моем ПК (32 логических ядра, 24 физических ядра). Но какое бы количество потоков я ни пробовал для тестирования, оно всегда было медленнее.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
// Nullable = tag is not guaranteed to appear in an XML element.
// Empty tags () would instead produce string.Empty.
public struct RawParameterData {
public ulong Id;
public string Name;
public ulong? OwnerComponentId;
public string Type;
public string Value;
public bool? Implicit;
public ulong ParentId;
public bool? Custom;
public override string ToString() => $"{Id} {OwnerComponentId} {Name} {Type} {Value} {Implicit} {ParentId} {Custom}";
}
// Just a set of methods to parse different data types from span of raw bytes
public static unsafe class RawPointerExtensions {
private static readonly Vector128 V16Lt = Vector128.Create((byte)' {
var p = (byte*)basePtr;
var a = borders[i];
var b = borders[i + 1];
threadLists[i] = RawPointerParser.ParseRange(p, a, b);
});
foreach (var t in threadLists) results.AddRange(t);
return results;
}
}
private static unsafe List FindChunkStarts(byte* ptr, long n, int threadCount, long start) {
var end = n - 10;
var work = end - start;
var slice = work / threadCount;
var starts = new List(threadCount + 1) { start };
for (var i = 1; i < threadCount; i++)
starts.Add(AlignToItem(ptr, start + slice * i, end));
starts.Add(end);
return starts;
}
private static unsafe long AlignToItem(byte* basePtr, long pos, long max) {
var p = basePtr + pos;
var end = basePtr + max - 5;
var m0 = Vector256.Create(Pattern[0]);
while (p < end) {
var v = Avx.LoadVector256(p);
var eq = Avx2.CompareEqual(v, m0);
var mask = Avx2.MoveMask(eq);
if (mask != 0) {
while (mask != 0) {
var idx = BitOperations.TrailingZeroCount(mask);
var q = p + idx;
if (q[4] == Pattern[4])
return q - basePtr + 1;
mask &= mask - 1;
}
}
p += 32;
}
return max;
}
}
[MemoryDiagnoser]
public class SampleParserBenchmark {
public string FilePath = @"C:\Parameter.xml";
[Benchmark] public List RawPointer() => RawPointerParser.ProcessFile(FilePath);
[Benchmark] public List RawPointerParallel() => RawPointerParserParallel.ProcessFile(FilePath);
[Benchmark] public List RawPointerParallel8() => RawPointerParserParallel.ProcessFile(FilePath, 8);
[Benchmark] public List RawPointerParallel16() => RawPointerParserParallel.ProcessFile(FilePath, 16);
}
public class Program {
public static void Main() => BenchmarkRunner.Run();
}
Результаты, которые я получил:
Метод
Среднее
Ошибка
StdDev
Gen0
Gen1
Gen2
Выделено
Хотя ожидается более высокое распределение, учитывая, что я работаю с массивом списков, это все еще даже близко не соответствует нашему последовательному подходу. И я озадачен, как такое может быть. Я пытался протестировать поиск фрагмента отдельно, и в среднем это заняло около 500 нс для 8 потоков, так что здесь дело не в этом.
Файл, о котором идет речь, если кто-то хочет повторить мой тест (16 МБ): https://drive.google.com/file/d/1vI6g9O ... sp=sharing
Вопросы: что приводит к замедлению работы моего параллельного подхода и что я могу сделать, чтобы устранить эти узкие места? Неужели самое эффективное решение — это просто потратить 20 мс на один поток?
В настоящее время я пытаюсь найти самый быстрый способ анализа XML-файла, структура которого хорошо известна (в основном данные игры, которые будут загружены инструментом). Я знаю разметку полностью, вплоть до того, сколько байтов данных я могу пропустить, чтобы получить следующее содержимое тега, и этот файл никогда не снабжается данными, предоставленными пользователем, поэтому вместо использования каких-либо (более медленных) классов, предназначенных для XML, таких как XmlReader и XElement, я просто выполняю синтаксический анализ необработанного указателя, перемещая заданное количество байтов вперед и либо проверяя тег (если это необязательно для данного элемента), либо просто считывая данные напрямую. Моя конечная цель — загрузить данные (список структур) как можно быстрее, не обращая внимания на любые опасения по поводу безопасности ввода и потенциальных осложнений в случае редактирования файла. Моим следующим предположением о том, как сделать это еще быстрее, было, естественно, распараллелить код, анализируя несколько его частей одновременно. Поскольку ни один XML-элемент не опирался на предыдущий, я мог безопасно анализировать равное (или почти равное) количество элементов в каждом потоке, а затем объединять коллекции. И хотя я знаю, что существуют определенные накладные расходы на запуск потоков (а в моем случае также на разделение работы на потоки), я все же ожидал, что параллельная версия превзойдет по производительности последовательную версию на моем ПК (32 логических ядра, 24 физических ядра). Но какое бы количество потоков я ни пробовал для тестирования, оно всегда было медленнее. [code]using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; using System.Text; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running;
// Nullable = tag is not guaranteed to appear in an XML element. // Empty tags () would instead produce string.Empty. public struct RawParameterData { public ulong Id; public string Name; public ulong? OwnerComponentId; public string Type; public string Value; public bool? Implicit; public ulong ParentId; public bool? Custom;
// Just a set of methods to parse different data types from span of raw bytes public static unsafe class RawPointerExtensions { private static readonly Vector128 V16Lt = Vector128.Create((byte)' { var p = (byte*)basePtr; var a = borders[i]; var b = borders[i + 1]; threadLists[i] = RawPointerParser.ParseRange(p, a, b); });
foreach (var t in threadLists) results.AddRange(t);
return results; } }
private static unsafe List FindChunkStarts(byte* ptr, long n, int threadCount, long start) { var end = n - 10; var work = end - start; var slice = work / threadCount;
var starts = new List(threadCount + 1) { start }; for (var i = 1; i < threadCount; i++) starts.Add(AlignToItem(ptr, start + slice * i, end)); starts.Add(end); return starts; }
private static unsafe long AlignToItem(byte* basePtr, long pos, long max) { var p = basePtr + pos; var end = basePtr + max - 5;
var m0 = Vector256.Create(Pattern[0]);
while (p < end) { var v = Avx.LoadVector256(p); var eq = Avx2.CompareEqual(v, m0); var mask = Avx2.MoveMask(eq); if (mask != 0) { while (mask != 0) { var idx = BitOperations.TrailingZeroCount(mask); var q = p + idx;
if (q[4] == Pattern[4]) return q - basePtr + 1;
mask &= mask - 1; } } p += 32; }
return max; } }
[MemoryDiagnoser] public class SampleParserBenchmark { public string FilePath = @"C:\Parameter.xml";
[Benchmark] public List RawPointer() => RawPointerParser.ProcessFile(FilePath);
[Benchmark] public List RawPointerParallel() => RawPointerParserParallel.ProcessFile(FilePath);
[Benchmark] public List RawPointerParallel8() => RawPointerParserParallel.ProcessFile(FilePath, 8);
[Benchmark] public List RawPointerParallel16() => RawPointerParserParallel.ProcessFile(FilePath, 16); }
public class Program { public static void Main() => BenchmarkRunner.Run(); } [/code] Результаты, которые я получил:
Метод Среднее Ошибка StdDev Gen0 Gen1 Gen2 Выделено
Хотя ожидается более высокое распределение, учитывая, что я работаю с массивом списков, это все еще даже близко не соответствует нашему последовательному подходу. И я озадачен, как такое может быть. Я пытался протестировать поиск фрагмента отдельно, и в среднем это заняло около 500 нс для 8 потоков, так что здесь дело не в этом. Файл, о котором идет речь, если кто-то хочет повторить мой тест (16 МБ): https://drive.google.com/file/d/1vI6g9OLtbgxSaks3LGbnHB3aE9dYX3Fn/view?usp=sharing Вопросы: что приводит к замедлению работы моего параллельного подхода и что я могу сделать, чтобы устранить эти узкие места? Неужели самое эффективное решение — это просто потратить 20 мс на один поток?