Следующий метод должен читать все значения из источника с индексами в ключах и сохранять их в назначении . Для простоты предположим, что длины ключей destination и равны, а все индексы в keys равны> = 0 и <<em> source. Длина . Длина ключей мала (<1000), длина <em>источника высокая (миллионы).
public static void Gather<T>(Span<T> destination, ReadOnlySpan<T> source,
ReadOnlySpan<int> keys)
{
for (int i = 0; i < keys.Length; i++)
{
destination[i] = source[keys[i]];
}
}
Какой метод будет эффективным в современных оборудование (SSE2, AVX2) для смешанного набора сценариев ios:
- ключи случайны
- ключи являются последовательными
- ключи частично последовательны
Benchmark
Обычно я использую Benchmark. net для микробенчмарков, но я не нашел способа проверить sh кеш процессора между операциями.
class Program
{
public class GatherTest
{
private readonly int[] sequentialKeys;
private readonly int[] partiallySequentialKeys;
private readonly int[] randomKeys;
private readonly int[] source;
private readonly int[] destination;
private const int KEYS_LENGTH = 1_000;
private const int SOURCE_LENGTH = 10_000_000;
public GatherTest()
{
var rng = new Random(0);
this.sequentialKeys = Enumerable.Range(0, KEYS_LENGTH).ToArray();
this.partiallySequentialKeys = Enumerable.Range(0, KEYS_LENGTH / 4).SelectMany(_ => Enumerable.Range(rng.Next(SOURCE_LENGTH - 4), 4)).ToArray();
this.randomKeys = Enumerable.Range(0, KEYS_LENGTH).Select(_ => rng.Next(SOURCE_LENGTH)).ToArray();
this.source = Enumerable.Range(0, SOURCE_LENGTH).ToArray();
this.destination = new int[KEYS_LENGTH];
}
public static void Gather<T>(Span<T> destination, ReadOnlySpan<T> source, ReadOnlySpan<int> keys)
{
for (int i = 0; i < keys.Length; i++)
{
destination[i] = source[keys[i]];
}
}
public static void Gather_Unnrolled<T>(Span<T> destination, ReadOnlySpan<T> source, ReadOnlySpan<int> keys)
{
int i = 0;
for (; i + 4 < keys.Length; i += 4)
{
destination[i] = source[keys[i]];
destination[i + 1] = source[keys[i + 1]];
destination[i + 2] = source[keys[i + 2]];
destination[i + 3] = source[keys[i + 3]];
}
for (; i < keys.Length; i++)
{
destination[i] = source[keys[i]];
}
}
public void FlushCache()
{
for (int i = 0; i < this.source.Length; i++) _ = this.source[i];
}
public void Gather_Sequential()
{
Gather<int>(this.destination, this.source, this.sequentialKeys);
}
public void Gather_Sequential_Unrolled()
{
Gather_Unnrolled<int>(this.destination, this.source, this.sequentialKeys);
}
public void Gather_Random()
{
Gather<int>(this.destination, this.source, this.randomKeys);
}
public void Gather_Random_Unrolled()
{
Gather_Unnrolled<int>(this.destination, this.source, this.randomKeys);
}
public void Gather_Partially_Sequential()
{
Gather<int>(this.destination, this.source, this.partiallySequentialKeys);
}
public void Gather_Partially_Sequential_Unrolled()
{
Gather_Unnrolled<int>(this.destination, this.source, this.partiallySequentialKeys);
}
public void Test(Action body, string name)
{
var sw = new Stopwatch();
for (int i = 0; i < 10000; i++)
{
this.FlushCache();
sw.Start();
body();
sw.Stop();
}
Console.WriteLine(name + ": " + sw.Elapsed.TotalMilliseconds+" ms");
}
}
static void Main(string[] args)
{
var benchmark = new GatherTest();
benchmark.Test(benchmark.Gather_Sequential, nameof(benchmark.Gather_Sequential));
benchmark.Test(benchmark.Gather_Sequential_Unrolled, nameof(benchmark.Gather_Sequential_Unrolled));
benchmark.Test(benchmark.Gather_Partially_Sequential, nameof(benchmark.Gather_Partially_Sequential));
benchmark.Test(benchmark.Gather_Partially_Sequential_Unrolled, nameof(benchmark.Gather_Partially_Sequential_Unrolled));
benchmark.Test(benchmark.Gather_Random, nameof(benchmark.Gather_Random));
benchmark.Test(benchmark.Gather_Random_Unrolled, nameof(benchmark.Gather_Random_Unrolled));
}
}
Вывод
Gather_Sequential: 18.8414 ms
Gather_Sequential_Unrolled: 18.0061 ms
Gather_Partially_Sequential: 63.0309 ms
Gather_Partially_Sequential_Unrolled: 54.9459 ms
Gather_Random: 127.5608 ms
Gather_Random_Unrolled: 116.4456 ms
Как видите, произвольный доступ намного медленнее, чем последовательный. Я предполагаю, что это связано с тем, что для каждого скопированного значения загружается вся строка кэша процессора.
Обновление: я попробовал инструкции AVX из System.Runtime.Intrinsics.X86 и протестировал функции в двух сценариях ios: cold ( значения не в cpu-cache) и теплые (значения уже в cpu-cache).
class Program
{
public class GatherTest
{
private readonly int[] sequentialKeys;
private readonly int[] partiallySequentialKeys;
private readonly int[] randomKeys;
private readonly int[] source;
private readonly int[] destination;
private const int KEYS_LENGTH = 1_000;
private const int SOURCE_LENGTH = 10_000_000;
public GatherTest()
{
var rng = new Random(0);
this.sequentialKeys = Enumerable.Range(0, KEYS_LENGTH).ToArray();
this.partiallySequentialKeys = Enumerable.Range(0, KEYS_LENGTH / 4).SelectMany(_ => Enumerable.Range(rng.Next(SOURCE_LENGTH - 4), 4)).ToArray();
this.randomKeys = Enumerable.Range(0, KEYS_LENGTH).Select(_ => rng.Next(SOURCE_LENGTH)).ToArray();
this.source = Enumerable.Range(0, SOURCE_LENGTH).ToArray();
this.destination = new int[KEYS_LENGTH];
}
public static void Gather<T>(Span<T> destination, ReadOnlySpan<T> source, ReadOnlySpan<int> keys)
{
for (int i = 0; i < keys.Length; i++)
{
destination[i] = source[keys[i]];
}
}
public static void Gather_Unnrolled<T>(Span<T> destination, ReadOnlySpan<T> source, ReadOnlySpan<int> keys)
{
int i = 0;
for (; i + 4 <= keys.Length; i += 4)
{
destination[i] = source[keys[i]];
destination[i + 1] = source[keys[i + 1]];
destination[i + 2] = source[keys[i + 2]];
destination[i + 3] = source[keys[i + 3]];
}
for (; i < keys.Length; i++)
{
destination[i] = source[keys[i]];
}
}
public static unsafe void Gather_Avx(Span<int> destination, ReadOnlySpan<int> source, ReadOnlySpan<int> keys)
{
var vectorLength = Vector256<int>.Count;
fixed (int* dst = destination)
fixed (int* src = source)
fixed (int* k = keys)
{
int i = 0;
for (; i + vectorLength <= keys.Length; i += vectorLength)
{
var keyVector = Avx.LoadVector256(k + i);
var destVector = Avx2.GatherVector256(src, keyVector, sizeof(int));
Avx.Store(dst + i, destVector);
}
for (; i < keys.Length; i++)
{
destination[i] = source[keys[i]];
}
}
}
public void FlushCache()
{
for (int i = 0; i < this.source.Length; i++) _ = this.source[i];
}
public void Gather_Sequential()
{
Gather<int>(this.destination, this.source, this.sequentialKeys);
}
public void Gather_Sequential_Unrolled()
{
Gather_Unnrolled<int>(this.destination, this.source, this.sequentialKeys);
}
public void Gather_Sequential_Avx()
{
Gather_Avx(this.destination, this.source, this.sequentialKeys);
}
public void Gather_Random()
{
Gather<int>(this.destination, this.source, this.randomKeys);
}
public void Gather_Random_Unrolled()
{
Gather_Unnrolled<int>(this.destination, this.source, this.randomKeys);
}
public void Gather_Random_Avx()
{
Gather_Avx(this.destination, this.source, this.randomKeys);
}
public void Gather_Partially_Sequential()
{
Gather<int>(this.destination, this.source, this.partiallySequentialKeys);
}
public void Gather_Partially_Sequential_Unrolled()
{
Gather_Unnrolled<int>(this.destination, this.source, this.partiallySequentialKeys);
}
public void Gather_Partially_Sequential_Avx()
{
Gather_Avx(this.destination, this.source, this.partiallySequentialKeys);
}
public void Test(Action body, string name)
{
this.Test(body, name + " (cold)", true);
this.Test(body, name + " (warm)", false);
}
public void Test(Action body, string name, bool flush)
{
var sw = new Stopwatch();
for (int i = 0; i < 10000; i++)
{
if (flush) this.FlushCache();
sw.Start();
body();
sw.Stop();
}
Console.WriteLine(name + ": " + sw.Elapsed.TotalMilliseconds+" ms");
}
}
static void Main(string[] args)
{
var benchmark = new GatherTest();
benchmark.Test(benchmark.Gather_Sequential, nameof(benchmark.Gather_Sequential));
benchmark.Test(benchmark.Gather_Sequential_Unrolled, nameof(benchmark.Gather_Sequential_Unrolled));
benchmark.Test(benchmark.Gather_Sequential_Avx, nameof(benchmark.Gather_Sequential_Avx));
benchmark.Test(benchmark.Gather_Partially_Sequential, nameof(benchmark.Gather_Partially_Sequential));
benchmark.Test(benchmark.Gather_Partially_Sequential_Unrolled, nameof(benchmark.Gather_Partially_Sequential_Unrolled));
benchmark.Test(benchmark.Gather_Partially_Sequential_Avx, nameof(benchmark.Gather_Partially_Sequential_Avx));
benchmark.Test(benchmark.Gather_Random, nameof(benchmark.Gather_Random));
benchmark.Test(benchmark.Gather_Random_Unrolled, nameof(benchmark.Gather_Random_Unrolled));
benchmark.Test(benchmark.Gather_Random_Avx, nameof(benchmark.Gather_Random_Avx));
}
}
Output
Gather_Sequential (cold): 18.3606 ms
Gather_Sequential (warm): 8.6091 ms
Gather_Sequential_Unrolled (cold): 16.8114 ms
Gather_Sequential_Unrolled (warm): 7.092 ms
Gather_Sequential_Avx (cold): 14.6569 ms
Gather_Sequential_Avx (warm): 2.4619 ms
Gather_Partially_Sequential (cold): 76.0622 ms
Gather_Partially_Sequential (warm): 12.1919 ms
Gather_Partially_Sequential_Unrolled (cold): 71.6684 ms
Gather_Partially_Sequential_Unrolled (warm): 8.1956 ms
Gather_Partially_Sequential_Avx (cold): 48.2327 ms
Gather_Partially_Sequential_Avx (warm): 4.2666 ms
Gather_Random (cold): 116.286 ms
Gather_Random (warm): 15.2227 ms
Gather_Random_Unrolled (cold): 109.3275 ms
Gather_Random_Unrolled (warm): 12.5555 ms
Gather_Random_Avx (cold): 110.4421 ms
Gather_Random_Avx (warm): 6.6867 ms
Хотя нет значительного ускорения в «холодный» сценарий, AVX действительно светит, если все значения уже находятся в cpu-cache. Остающиеся проблемы:
- Вы должны использовать небезопасный код, который может быть невозможен в вашем проекте
- Невозможно иметь единственную общую c версию функции, даже с неуправляемым ограничением
- Тестовая функция здесь для AVX не является надежной (без проверки функций процессора, может не работать при неправильном выравнивании), поэтому, пожалуйста, не используйте ее в текущем форма