I have to do a large number of aggregation operations, with the output grouped by some dimension (int/byte ID). I'm using C#, but hopefully I can still get good advice from the majority C++ crowd reading this :)
A simplified version is below:
public static (double[], double[]) AggregateDataGroupBy(double[] data, double[] weight, byte[] dimension)
{
int numberOfValues = byte.MaxValue - byte.MinValue + 1;
double[] totalValue = new double[numberOfValues];
double[] totalWeight = new double[numberOfValues];
for (int i = 0; i < data.Length; i++)
{
byte index = dimension[i];
totalValue[index] += data[i];
totalWeight[index] += weight[i];
}
return (totalValue, totalWeight);
}
SIMD vectorization gives a significant speed-up when no needing to group over the dimension. My first attempt to vectorise the operation was to grab the running totals for the dimensions of the 4 rows being processed, load the input vectors using a gather, do the aggregation functions and then scatter back. As scatter isn't part of AVX2, this last part is particularly slow.
public static unsafe (double[], double[]) AggregateDataGather(double[] data, double[] weight, int[] dimension)
{
int numberOfValues = 256;
double[] totalValue = new double[numberOfValues];
double[] totalWeight = new double[numberOfValues];
if (Avx2.IsSupported)
{
int vectorSize = 256 / 8 / sizeof(double);
int i;
fixed (double* ptr = data, ptr2 = weight, ptrValue = totalValue, ptrWeight = totalWeight)
{
fixed (int* dimptr = dimension)
{
var accValue = stackalloc double[vectorSize];
var accWeight = stackalloc double[vectorSize];
for (i = 0; i <= data.Length - vectorSize; i += vectorSize)
{
Vector128<int> indices = Avx2.LoadVector128(dimptr + i);
var accVectorV = Avx2.GatherVector256(ptrValue, indices, 8);
var accVectorW = Avx2.GatherVector256(ptrWeight, indices, 8);
var v = Avx2.LoadVector256(ptr + i);
var w = Avx2.LoadVector256(ptr2 + i);
accVectorV = Avx2.Add(accVectorV, v);
accVectorW = Avx2.Add(accVectorW, w);
Avx2.Store(accValue, accVectorV);
Avx2.Store(accWeight, accVectorW);
for (int ii = 0; ii < vectorSize; ii++)
{
var index = dimptr[i + ii];
totalValue[index] = accValue[ii];
totalWeight[index] = accWeight[ii];
}
}
}
}
}
else if (Avx.IsSupported || Sse42.IsSupported)
{
// Do other stuff
}
return (totalValue, totalWeight);
}
(Please excuse the change of dimension from byte to int - I tested both and both are slower)
The intrinsics version above runs slower than the naive algorithm on my Ryzen 3600. (268ms for 100m values, rather than 230ms)
Given that my data only changes after many aggregations (over hundreds/thousands of different dimensions), I find that my fastest implementation can be to store the data (value, weight) in a vector and do a naive group-by. This gives similar performance on the Ryzen, but is 10% faster on an older i7 (without AVX).
public static Vector2[] AggregateData(Vector2[] data, byte[] dimension)
{
int numberOfValues = byte.MaxValue - byte.MinValue + 1;
Vector2[] sum = new Vector2[numberOfValues];
for (int i = 0; i < data.Length; i++)
{
sum[dimension[i]] += data[i];
}
return sum;
}
I'd read some papers on histogram functions that simply count the number of occurences of each dimension. They got a close to perfect 8x speed up compared to naive approaches.
Have I missed something in my attempt to use AVX2 intrinsics? Am I always going to be faced with inefficient gather/scatter operations? Any comments/suggestions?
As a sub-case, are there strategies that will only work when the dimension size is small (processing 4 dimension values at a time)? E.g. loading the value into a vector with a single non-zero value, as follows, and optimising the number of rows being processed at a time to use all the cache memory.
Values (11, 12, 13, 14, 15, 16, 17)
Indicies (1, 0, 3, 1, 2, 0, 3)
=>
<0, 11, 0, 0>
+ <12, 0, 0, 0>
+ <0, 0, 0, 13>
+ <0, 14, 0, 0>
+ <0, 0, 15, 0>
+ <16, 0, 0, 0>
+ <0, 0, 0, 17>
(This didn't appear to me a likely solution, due to inefficiency once the dimension size increases. So I haven't tried it yet, but I will if it's suggested as an efficient work-around.)