C#. Почему мой параллельный анализатор необработанных указателей XML работает медленнее?C#

Место общения программистов C#
Ответить
Anonymous
 C#. Почему мой параллельный анализатор необработанных указателей XML работает медленнее?

Сообщение Anonymous »

В настоящее время я пытаюсь найти самый быстрый способ анализа 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();
}
Результаты, которые я получил:

Код: Выделить всё

| Method               | Mean     | Error    | StdDev   | Gen0      | Gen1      | Gen2     | Allocated |
|--------------------- |---------:|---------:|---------:|----------:|----------:|---------:|----------:|
| RawPointer           | 21.28 ms | 0.413 ms | 0.701 ms | 1812.5000 | 1781.2500 | 937.5000 |  40.17 MB |
| RawPointerParallel   | 26.65 ms | 0.508 ms | 0.522 ms | 1593.7500 | 1562.5000 | 687.5000 |  53.86 MB |
| RawPointerParallel8  | 23.29 ms | 0.550 ms | 1.623 ms | 1750.0000 | 1718.7500 | 812.5000 |  47.93 MB |
| RawPointerParallel16 | 23.03 ms | 0.556 ms | 1.640 ms | 1750.0000 | 1718.7500 | 750.0000 |  48.44 MB |
Хотя ожидается более высокое распределение, учитывая, что я работаю с массивом списков, это все еще даже близко не соответствует нашему последовательному подходу. И я озадачен, как такое может быть. Я пытался протестировать поиск фрагмента отдельно, и в среднем это заняло около 500 нс для 8 потоков, так что здесь дело не в этом.
Файл, о котором идет речь, если кто-то хочет повторить мой тест (16 МБ): https://drive.google.com/file/d/1vI6g9O ... sp=sharing
Вопросы: что приводит к замедлению работы моего параллельного подхода и что я могу сделать, чтобы устранить эти узкие места? Неужели самое эффективное решение — это просто потратить 20 мс на один поток?

Подробнее здесь: https://stackoverflow.com/questions/798 ... ork-slower
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «C#»