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

Место общения программистов C#
Ответить
Anonymous
 Почему мой параллельный анализатор необработанных указателей 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();
}
Результаты, которые я получил:



Метод
Среднее
Ошибка
StdDev
Gen0
Gen1
Gen2
Выделено




RawPointer
21,28 мс
0,413 мс
0,701 мс
1812.5000
1781.2500
937.5000
40,17 МБ


RawPointerParallel
26,65 мс
0,508 мс
0,522 мс
1593,7500
1562,5000
687,5000
53,86 МБ


RawPointerParallel8
23,29 мс
0,550 мс
1,623 мс
1750,0000
1718,7500
812,5000
47,93 МБ


RawPointerParallel16
23,03 мс
0,556 мс
1,640 мс
1750.0000
1718.7500
750.0000
48,44 МБ



Хотя ожидается более высокое распределение, учитывая, что я работаю с массивом списков, это все еще даже близко не соответствует нашему последовательному подходу. И я озадачен, как такое может быть. Я пытался протестировать поиск фрагмента отдельно, и в среднем это заняло около 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#»