【发布时间】:2013-03-11 09:04:49
【问题描述】:
是否有任何可靠的方法可以强制 GCC(或任何编译器)在循环之外(其中该大小不是编译时常量,而是该循环内的常量)中排除运行时大小检查 memcpy(),专门循环检查每个相关的尺寸范围,而不是重复检查其中的尺寸?
这是一个从性能回归报告中减少的测试用例here,该测试用例是一个开源库,旨在对大型数据集进行有效的内存分析。 (回归恰好是因为我的一个提交......)
原始代码在 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));
}
}
(原始代码中不存在断言;相反,它会检查连续性并在可能的情况下调用优化版本,如果不是,则调用未优化版本。)
此版本在大多数情况下优化得非常好,因为对于小n 和大k 的正常用例。然而,相反的用例也确实发生了(大的n 和小的k),结果是n == 10000 和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(),但仍然优化已知连续的小数组的情况(因此在大多数情况下它比原来的要快,因为原来的不做这样的假设)。
【问题讨论】:
标签: c++ c performance compiler-optimization memcpy