是否有任何可靠的方法可以强制 GCC(或任何编译器)在memcpy()
循环外部排除运行时大小检查(其中该大小不是编译时常量,而是该循环内的常量),针对每个相关大小范围专门循环而不是反复检查里面的大小?
这是一个测试用例,从这里报告的性能回归中减少,这是一个开源库,旨在对大型数据集进行有效的内存分析。(回归恰好是因为我的一个提交......)
原始代码在 Cython 中,但我已将其简化为纯 C 代理,如下所示:
void take(double * out, double * in,
int stride_out_0, int stride_out_1,
int stride_in_0, int stride_in_1,
int * indexer, int n, int k)
{
int i, idx, j, k_local;
k_local = k; /* prevent aliasing */
for(i = 0; i < n; ++i) {
idx = indexer[i];
for(j = 0; j < k_local; ++j)
out[i * stride_out_0 + j * stride_out_1] =
in[idx * stride_in_0 + j * stride_in_1];
}
}
步幅是可变的;通常,甚至不能保证数组是连续的(因为它们可能是较大数组的不连续切片。)但是,对于 c 连续数组的特殊情况,我已将上述优化为以下内容:
void take(double * out, double * in,
int stride_out_0, int stride_out_1,
int stride_in_0, int stride_in_1,
int * indexer, int n, int k)
{
int i, idx, k_local;
assert(stride_out_0 == k);
assert(stride_out_0 == stride_in_0);
assert(stride_out_1 == 1);
assert(stride_out_1 == stride_in_1);
k_local = k; /* prevent aliasing */
for(i = 0; i < n; ++i) {
idx = indexer[i];
memcpy(&out[i * k_local], &in[idx * k_local],
k_local * sizeof(double));
}
}
(原始代码中不存在断言;相反,它会检查连续性并在可能的情况下调用优化版本,如果不是,则调用未优化版本。)
这个版本在大多数情况下优化得非常好,因为对于 smalln
和 large的正常用例k
。然而,相反的用例也确实发生了(大n
和小k
),事实证明,对于n == 10000
and的特定情况k == 4
(不能排除它代表假设工作流的重要部分),memcpy()
版本是 3.6 倍比原来慢。显然,这主要是因为它k
不是编译时常数,这一事实证明了下一个版本的性能(几乎或完全取决于优化设置)以及原始版本(或有时更好),对于特殊情况k == 4
:
if (k_local == 4) {
/* this optimizes */
for(i = 0; i < n; ++i) {
idx = indexer[i];
memcpy(&out[i * k_local], &in[idx * k_local],
k_local * sizeof(double));
}
} else {
for(i = 0; i < n; ++i) {
idx = indexer[i];
memcpy(&out[i * k_local], &in[idx * k_local],
k_local * sizeof(double));
}
}
显然,为 的每个特定值硬编码一个循环是不切实际的k
,所以我尝试了以下方法(作为第一次尝试,如果它有效,以后可以推广):
if (k_local >= 0 && k_local <= 4) {
/* this does not not optimize */
for(i = 0; i < n; ++i) {
idx = indexer[i];
memcpy(&out[i * k_local], &in[idx * k_local],
k_local * sizeof(double));
}
} else {
for(i = 0; i < n; ++i) {
idx = indexer[i];
memcpy(&out[i * k_local], &in[idx * k_local],
k_local * sizeof(double));
}
}
不幸的是,最后一个版本并不比原始memcpy()
版本快,这让我对 GCC 优化能力的信心有些沮丧。
有什么方法可以给 GCC 额外的“提示”(通过任何方式),这将帮助它在这里做正确的事情?(更好的是,是否有“提示”可以可靠地跨不同的编译器工作?这个库是为许多不同的目标编译的。)
引用的结果适用于带有“-O2”标志的 32 位 Ubuntu 上的 GCC 4.6.3,但我也测试了具有相似(但不相同)结果的 GCC 4.7.2 和“-O3”版本。我已经将我的测试工具发布到LiveWorkspace,但是时间来自我自己的机器使用time(1)
命令(我不知道 LiveWorkspace 时间有多可靠。)
编辑:我也考虑过为一些最小尺寸设置一个“幻数”来调用memcpy()
,我可以通过重复测试找到这样一个值,但我不确定我的结果在不同编译器/平台上的通用性如何. 我可以在这里使用任何经验法则吗?
进一步编辑:实际上,在这种情况下,意识到k_local
变量是无用的,因为不可能有别名;这是从我在可能的地方(k
是全球性的)进行的一些实验中减少的,我忘记了我改变了它。忽略那部分。
编辑标签:意识到我也可以在较新版本的 Cython 中使用 C++,所以标记为 C++,以防 C++ 有任何帮助...
最终编辑:代替(现在)下降到专业memcpy()
的组装,以下似乎是我本地机器的最佳经验解决方案:
int i, idx, j;
double * subout, * subin;
assert(stride_out_1 == 1);
assert(stride_out_1 == stride_in_1);
if (k < 32 /* i.e. 256 bytes: magic! */) {
for(i = 0; i < n; ++i) {
idx = indexer[i];
subout = &out[i * stride_out_0];
subin = &in[idx * stride_in_0];
for(j = 0; j < k; ++j)
subout[j] = subin[j];
}
} else {
for(i = 0; i < n; ++i) {
idx = indexer[i];
subout = &out[i * stride_out_0];
subin = &in[idx * stride_in_0];
memcpy(subout, subin, k * sizeof(double));
}
}
这使用“幻数”来决定是否调用memcpy()
,但仍然优化已知连续的小数组的情况(因此在大多数情况下它比原始数组更快,因为原始数组没有做出这样的假设)。