The current example shows an execution of N=10 independent operations inside a three-layered loop, but unfortunately the intel compiler autovectorization computes the cost at loop level, and when successfull at the innermost level it refuses to consider the vectorization of the two 'external' loops.
A solution would be to express the same with only one loop, where 'if-else' conditions are allowed but perhaps not encouraged.
The indexing covers a subset of the total possible combinations, if someone knows the formal name of it it would be nice.
A working minimal example for L=5 (i.e. 10 possible combinations) follows. The production-code makes heavy use of the case with L=6, which has 20 operations that are performed ~100k times (i.e. for different arrays of length 6).
Note: auto-vectorization of the innermost loop has been tested without the prints using std::cout at each iteration :-)
Note: for curiosity, the (current) strategy of precomputing the elements (with only the inner loop vectorized, intel report showing "estimated potential speedup: 1.8") as opposed to computing them on the fly only when needed is making the section 12% slower.
#include <iostream>
int vectorizable_function(int &A, int &B, int &C) { return A + B - C; }
void populate_combinations(int *container, int L, int *data){
// Intel vectorization report provides feedback
//
//
int counter=0;
for (int i=0; i<L-2; i++){
for (int j=i+1; j<L-1; j++){
for (int k=j+1; k<L; k++){
// std::cout << "i,j,k are " << i << " " << j << " " << k << '\n';
container[counter] = vectorizable_function( data[i], data[j], data[k] );
counter++;
}
}
}
std::cout << "counter was " << counter << std::endl;
}
// Function we want to populate with the parallelizable algorithm
//
void vectorizable_populate_combinationsL5(int * container, int *data){
#pragma vector always
for (int i=0; i<10; i++){
// Smart algorithm goes here?
}
}
int main(){
const int L = 5;
const int TOTALNCOMBINATIONS = 10;
int combinations[10];
int data[5] = {5,6,8,12,3};
populate_combinations(combinations, L, data);
std::cout << "Results are:\n";
for (int i=0; i<TOTALNCOMBINATIONS; i++) std::cout << combinations[i] << std::endl;
}
Responding to a comment:
Is your real function also separable the way this one is, where you can compute data[i] + data[j] and reuse it for all data[k]? Also commutative between i and j since they're both getting added, only the k element is getting subtracted. Although that probably doesn't matter, since j runs from [i+1 .. L).
The real function is shown in the following fragment:
double dxAll[18]; // declared and populated long before it's needed
int counter = 0;
for (int i=0; i<4; i++){
for (int j=i+1; j<5; j++){
for (int k=j+1; k<6; k++){
denomAll[counter] = dxAll[i]*dxAll[6 + j]*dxAll[12 + k] +
dxAll[6 + i]*dxAll[12 + j]*dxAll[k] +
dxAll[12 + i]*dxAll[j]*dxAll[6 + k] -
dxAll[i]*dxAll[12 + j]*dxAll[6 + k] -
dxAll[6 + i]*dxAll[j]*dxAll[12 + k] -
dxAll[12 + i]*dxAll[6 + j]*dxAll[k];
counter++;
}
}
}