有什么方法可以加速此功能:
void task(int I, int J, int K, int *L, int **ids, double *bar){
double *foo[K];
for (int k=0;k<K;k++)
foo[k] = new double[I*L[k]];
// I am filling these arrays somehow
// This is not a bottleneck, hence omitted here
for (int i=0;i<I;i++)
for (int j=0;j<J;j++){
double tmp = 1.;
for (int k=0;k<K;k++)
tmp *= foo[k][i*L[k]+ids[j][k]]; //ids[j][k]<L[k]
bar[i*J+j] = tmp;
}
}
典型值为:I = 100,000
,J = 10,000
,K=3
,L=[50,20,60]
。
我了解到__restrict__
关键字/扩展名可能会有所帮助,但不确定如何在此处应用它。例如,尝试将其放入foo[k] = new double[...]
的定义中,我得到error: '__restrict_ qualifiers cannot be applied to double
。此外,我不知道是否应该/如何声明ids
和ids[j], 1<= j<= J
为受限对象。
请注意,在我的实际代码中,我在CPU具有内核的多个线程中并行执行这些任务。
我主要在编写与C兼容的C ++,因此欢迎使用两种语言的解决方案。
答案 0 :(得分:2)
https://en.cppreference.com/w/c/language/restrict声称您可以声明一个restrict
指针数组,使其像C99 / C11中那样加倍:
typedef double *array_t[10];
restrict array_t foo; // the type of a is double *restrict[10]
但是只有gcc接受。我认为这是GCC主义,不是有效的ISO C11。 (gcc也接受
array_t restrict foo_r;
,但没有其他编译器接受。)
ICC警告"restrict" is not allowed
,叮当声拒绝了
<source>:16:5: error: restrict requires a pointer or reference ('array_t' (aka 'double *[10]') is invalid)
restrict array_t foo_r;
^
MSVC使用error C2219: syntax error: type qualifier must be after '*'
拒绝它
这些带有__restrict
的编译器在C ++中的行为基本相同,它们被接受为C ++扩展,其语义与C99 restrict
相同。
作为一种解决方法,您可以每次从foo
读取数据时都使用合格的临时指针,而不是f[k][stuff]
。我认为这可以保证您引用的内存通过fk
进行访问与通过声明fk
的块内的任何其他指针访问的内存不同。
double *__restrict fk = foo[k];
tmp *= fk[ stuff ];
我不知道如何向编译器保证f[0..K-1]
指针都不互为别名。我不认为这能达到目的。
您不需要__restrict。
根据Godbolt编译器浏览器https://godbolt.org/z/4YjlDA上的diff窗格,我在所有指针声明中都添加了__restrict
,例如int *__restrict *__restrict ids
,它根本没有改变asm。 正如我们期望的那样,因为基于类型的别名使编译器假设double
中存储的bar[]
不会修改int *
的任何int *ids[]
元素就像人们在评论中所说的那样,这里没有别名,表明编译器无法进行排序。实际上,它看起来可以 进行整理,而无需任何额外的指针重载。
它也不能别名*foo[k]
,因为我们在此函数中获得了带有new
的指针。他们不能指向bar[]
内。
(所有主要的x86 C ++编译器(GCC,clang,ICC,MSVC)在C ++中都支持__restrict
,其行为与C99 restrict
相同:对通过此指针存储的编译器的承诺don请勿修改其他指针指向的对象。
我建议使用__restrict
而不是__restrict__
,至少如果您最希望跨x86编译器进行移植。我不确定那是不是。)
您似乎在说要尝试将__restrict__
放入任务而不是声明中。那是行不通的,__restrict
适用于指针变量本身,而不是单个赋值。
该问题的第一个版本在内部循环中存在一个错误:它具有K++
而不是k++
,因此这是纯粹的未定义行为,并且编译器很奇怪。汇编没有任何意义(例如,即使foo[]
是一个函数arg,也没有FP乘法指令)。这就是为什么最好使用klen
之类的名称代替K
作为数组维。
在Godbolt链接上修复该错误之后,在所有组件上都带有/不带有__restrict
的asm仍然没有区别,但这更加理智。
顺便说一句,将double *foo[]
设为函数arg可以让我们仅查看主循环的asm。而且您实际上需要__restrict
,因为存储到bar[]
的存储区可能会修改foo[][]
的元素。这在您的函数中不会发生,因为编译器知道new
的内存不是任何现有指针指向的,但是它不知道是否foo
是一个函数arg。
循环中有少量工作是对32位int
结果进行符号扩展,然后将它们用作具有64位指针的数组索引。这在某处增加了一个延迟周期,但不是循环承载的FP乘法依赖链,因此可能无关紧要。通过使用size_t k=0;
作为最内层的循环计数器,可以摆脱x86-64上内层循环内的一条指令。 L[]
是一个32位数组,因此i*L[k]
需要在循环内进行符号扩展。在x86-64上,从32位零扩展到64位是免费的,因此i * (unsigned)L[k]
在指针追随的dep链中保存了一条movsx
指令。然后,gcc8.2进行的内部循环是您讨厌的数据结构/布局所需的所有必要工作。 https://godbolt.org/z/bzVSZ7
我不知道这是否会有所作为。我认为导致高速缓存未命中的内存访问模式更可能是您遇到真实数据的瓶颈。
由于数据不连续,它也无法自动矢量化。但是,无法通过遍历j
或i
来获取连续的源数据。至少i
是一个简单的跨步,而不必重做ids[j][k]
。
如果生成转置的foo[k][...]
和bar[...]
,因此用foo[k][ i + L[k] * ids[j][k] ]
进行索引,那么src和dst中将有连续的内存,因此您(或编译器)可以使用SIMD相乘。
答案 1 :(得分:0)
restrict
在这种情况下无关紧要。
您的算法很垃圾,并且不允许使用长向量运算(因此,微优化在这里根本无济于事)。
您需要找到内部循环中的元素占据数组索引的连续块的方式。完成此操作后,编译器必须从数组中的不同位置读取每个元素,这使编译器无法展开循环和更长的向量指令。这也可能是非常不友好的高速缓存存储器。
首先重新考虑算法-如果算法效率极低,过早的优化将无济于事
在OP评论之后,我只想向他展示“天真”和更高效(天真少但难于理解)之间的区别
让我们考虑32位无符号值的奇偶校验。天真的方法:
int very_naive_parity(const uint32_t val)
{
unsigned parity = 0;
for(unsigned bit = 0; bit < 32; bit++)
{
if(val & (1U << bit))
{
parity = !parity;
}
}
return parity;
}
这很容易编写和理解,但是效率极低。将至少执行288条指令来计算此奇偶校验。
更高效:
int parity(const uint32_t val)
{
uint32_t tmp = val;
tmp ^= tmp >> 16;
tmp ^= tmp >> 8;
tmp ^= tmp >> 4;
return (0b110100110010110 >> (tmp & 0x0f)) & 1;
}
将以9条指令执行(两种计算均不包含函数序言和结语) 更难理解吗? -当然可以。但正如我所写,效率通常对人类而言并不那么容易。