我有一个消耗cpu的代码,其中一些循环函数执行很多次。此循环中的每个优化都会带来显着的性能提升。问题:你如何优化这个循环(尽管优化的目的还不多......)?
void theloop(int64_t in[], int64_t out[], size_t N)
{
for(uint32_t i = 0; i < N; i++) {
int64_t v = in[i];
max += v;
if (v > max) max = v;
out[i] = max;
}
}
我尝试了一些事情,例如我用每个循环中递增的指针替换了数组,但(令人惊讶的是)我丢失了一些性能而不是获得......
编辑:
itsMaximums
,错误)int64_t
,所以是消极和积极的N
在编译时未知_m128i
,执行和存储的开销高于SSE速度增益。但我不是专家SSE,所以也许我的代码很差)结果:
我添加了一些循环展开,和来自Alex'es帖子的一个很好的黑客。下面我粘贴一些结果:
for(size_t i = 1; i < N; i+=CHUNK) {
int64_t t_in0 = in[i+0];
int64_t t_in1 = in[i+1];
int64_t t_in2 = in[i+2];
int64_t t_in3 = in[i+3];
max &= -max >> 63;
max += t_in0;
out[i+0] = max;
max &= -max >> 63;
max += t_in1;
out[i+1] = max;
max &= -max >> 63;
max += t_in2;
out[i+2] = max;
max &= -max >> 63;
max += t_in3;
out[i+3] = max;
}
答案 0 :(得分:15)
首先,您需要查看生成的程序集。否则,当执行此循环时,您无法知道发生了什么。
现在:这个代码是在64位计算机上运行的吗?如果没有,那些64位的添加可能会有点伤害。
这个循环似乎是使用SIMD指令的明显候选者。 SSE2支持许多用于整数算术的SIMD指令,包括一些可用于两个64位值的指令。
除此之外,看看编译器是否正确地展开循环,如果没有,请自行完成。展开循环的几次迭代,然后重新排序它的地狱。将所有内存负载放在循环的顶部,这样它们就可以尽早启动。
对于if
行,检查编译器是否正在生成条件移动,而不是分支。
最后,看看您的编译器是否支持restrict
/ __restrict
关键字。它在C ++中不是标准的,但非常对于向编译器指示in
和out
没有指向相同的地址非常有用。
编译时是否知道大小(N
)?如果是这样,请将其设为模板参数(然后尝试将in
和out
作为对正确大小的数组的引用,因为这也可以帮助编译器进行别名分析)
我的头脑中只有一些想法。但是,再次研究拆卸。您需要知道编译器为您做了什么,尤其是不为您做的事情。
修改
与您的编辑:
max &= -max >> 63;
max += t_in0;
out[i+0] = max;
让我印象深刻的是你添加了一个巨大的依赖链。 在计算结果之前,必须取消max,必须移位结果, 的结果必须和'与其原始值一起使用,结果 必须添加到另一个变量。
换句话说,所有这些操作都必须序列化。在上一次完成之前,您无法启动其中一个。这不一定是加速。现代流水线无序CPU喜欢并行执行大量事务。用一长串依赖指令来绑定它是你可以做的最严重的事情之一。 (当然,如果它可以与其他迭代交错,它可能会更好。但我的直觉是一个简单的条件移动指令会更好)
答案 1 :(得分:7)
<子> 子>
#公告请参阅chat嗨Jakub,如果我找到一个使用启发式优化的版本,如果统一分布的随机数据将导致
int64_t
的速度提高约3.2倍(10.56x有效使用),你会怎么说?float
S)?
我还没有时间来更新帖子,但可以通过聊天找到解释和代码。编辑:具有讽刺意味的是......测试平台有一个致命的缺陷,导致结果无效:启发式版本实际上是跳过部分输入,但因为现有的输出不是清除后,它似乎有正确的输出......(仍在编辑......)
我使用相同的测试床代码(下面)来验证结果是否正确并且与OP的原始实现完全匹配
好的,我已根据您的代码版本发布了一个基准测试,并且我还建议使用partial_sum
。
在此处查找所有代码 https://gist.github.com/1368992#file_test.cpp
对于
的默认配置#define MAGNITUDE 20
#define ITERATIONS 1024
#define VERIFICATION 1
#define VERBOSE 0
#define LIMITED_RANGE 0 // hide difference in output due to absense of overflows
#define USE_FLOATS 0
它会(在此处 output fragment ):
int64_t
)有许多(令人惊讶或不足为奇)的结果:
如果您在启用优化的情况下进行编译,那么任何算法( for integer data )之间的没有显着的性能差异。 (见Makefile;我的拱门是64位,英特尔酷睿Q9550和gcc-4.6.1)
算法不等同(你会看到哈希值不同):值得注意的是Alex提出的位小提琴不会以完全相同的方式处理整数溢出(这可能是隐藏的定义
#define LIMITED_RANGE 1
限制输入数据,因此不会发生溢出;请注意,partial_sum_incorrect
版本显示了产生相同不同结果的等效C ++非按位_算术运算:
return max<0 ? v : max + v;
也许,你的目的没问题?)
令人惊讶一次计算max算法的两个定义并不是更昂贵。你可以在partial_sum_correct
内看到这一点:它在同一个循环中计算max的'公式';这真的不过是一个triva,因为这两种方法都没有明显更快......
当您能够使用float
代替int64_t
时,可以获得更强大的大幅提升。快速而肮脏的黑客可以应用于基准
#define USE_FLOATS 0
显示使用partial_sum_incorrect
代替float
时,基于STL的算法(int64_t
)大约快2.5倍。
/> 注意:强>
partial_sum_incorrect
的命名仅涉及整数溢出,不适用于浮点数;从哈希匹配的事实可以看出这一点,所以事实上它是_partial_sum_float_correct_:)partial_sum_correct
的实现正在进行双重工作,导致它在浮点模式下表现不佳。请参阅项目符号 3。 (我之前提到的OP中的循环展开版本中有一个错误的错误)
为了您的兴趣,部分和应用程序在C ++ 11中看起来像这样:
std::partial_sum(data.begin(), data.end(), output.begin(),
[](int64_t max, int64_t v) -> int64_t
{
max += v;
if (v > max) max = v;
return max;
});
答案 2 :(得分:5)
有时,您需要退后一步并再次查看它。第一个问题显然是,你需要这个吗?是否会有更好的替代算法?
话虽如此,并且为了这个问题而假设你已经确定了这个算法,我们可以尝试推理我们实际拥有的东西。
免责声明:我所描述的方法受到Tim Peters用于改进传统内部实施的成功方法的启发,导致TimSort。所以请耐心等待我;)
<强> 1。提取属性
我可以看到的主要问题是迭代之间的依赖关系,这将阻止许多可能的优化,并阻碍许多并行尝试。
int64_t v = in[i];
max += v;
if (v > max) max = v;
out[i] = max;
让我们以功能性的方式重新编写代码:
max = calc(in[i], max);
out[i] = max;
其中:
int64_t calc(int64_t const in, int64_t const max) {
int64_t const bumped = max + in;
return in > bumped ? in : bumped;
}
或者更确切地说,是一个简化版本(baring溢出,因为它未定义):
int64_t calc(int64_t const in, int64_t const max) {
return 0 > max ? in : max + in;
}
您是否注意到提示点?行为的变化取决于名字不同的(*)max
是正面还是负面。
这个临界点让您更有意思地观察in
中的值,特别是根据它们对max
的影响:
max < 0
和in[i] < 0
然后out[i] = in[i] < 0
max < 0
和in[i] > 0
然后out[i] = in[i] > 0
max > 0
和in[i] < 0
然后out[i] = (max + in[i]) ?? 0
max > 0
和in[i] > 0
然后out[i] = (max + in[i]) > 0
(*)名字错误,因为它也是一个名字隐藏的累加器。我没有更好的建议。
<强> 2。优化运营
这引导我们发现有趣的案例:
[i, j)
只包含负值(我们称之为负片),那么我们可以执行std::copy(in + i, in + j, out + i)
和max = out[j-1]
[i, j)
只包含正值,则它是纯积累代码(可以很容易地展开)max
只要in[i]
为正面就会获得肯定
因此,在实际使用输入之前建立输入的配置文件可能很有趣(但可能不是,我没有做出承诺)。请注意,对于大型输入,可以通过块来创建配置文件块,例如,根据缓存行大小调整块大小。
对于参考,3个例程:
void copy(int64_t const in[], int64_t out[],
size_t const begin, size_t const end)
{
std::copy(in + begin, in + end, out + begin);
} // copy
void accumulate(int64_t const in[], int64_t out[],
size_t const begin, size_t const end)
{
assert(begin != 0);
int64_t max = out[begin-1];
for (size_t i = begin; i != end; ++i) {
max += in[i];
out[i] = max;
}
} // accumulate
void regular(int64_t const in[], int64_t out[],
size_t const begin, size_t const end)
{
assert(begin != 0);
int64_t max = out[begin - 1];
for (size_t i = begin; i != end; ++i)
{
max = 0 > max ? in[i] : max + in[i];
out[i] = max;
}
}
现在,假设我们可以使用简单的结构以某种方式表征输入:
struct Slice {
enum class Type { Negative, Neutral, Positive };
Type type;
size_t begin;
size_t end;
};
typedef void (*Func)(int64_t const[], int64_t[], size_t, size_t);
Func select(Type t) {
switch(t) {
case Type::Negative: return ©
case Type::Neutral: return ®ular;
case Type::Positive: return &accumulate;
}
}
void theLoop(std::vector<Slice> const& slices, int64_t const in[], int64_t out[]) {
for (Slice const& slice: slices) {
Func const f = select(slice.type);
(*f)(in, out, slice.begin, slice.end);
}
}
现在,除非内部循环中的工作很少,否则计算特征可能成本太高......但是它很好地导致并行化。
第3。简单的并行化
请注意,表征是输入的纯函数。因此,假设您以每块方式工作,可以并行:
Slice::Type
值即使输入本质上是随机的,提供块也足够小(例如,CPU L1缓存线),它可能有一些块可以工作。两个线程之间的同步可以通过Slice
(生产者/消费者)的简单线程安全队列完成,并添加bool last
属性以停止使用或在向量中创建Slice
使用Unknown
类型,并使用消费者阻止直到它已知(使用原子)。
注意:因为表征是纯粹的,所以它是令人尴尬的平行。
<强> 4。更多并行化:投机工作
请记住这个无辜的评论:{em> max
只要in[i]
为正面就会得到肯定。
假设我们可以(可靠地)猜测Slice[j-1]
将产生负值的max
值,那么Slice[j]
上的计算与它们之前的计算无关,我们可以立即开始工作!
当然,这是一个猜测,所以我们可能错了......但是一旦我们完全表征了所有的切片,我们就有了空闲核心,所以我们不妨用它们进行投机工作!如果我们错了?好吧,消费者线程只会轻轻地抹去我们的错误并用正确的值替换它。
推测性地计算Slice
的启发式应该很简单,并且必须进行调整。它也可能是适应性的......但这可能更难!
<强>结论强>
分析您的数据集并尝试查找是否可能破坏依赖关系。如果它是你可以利用它,即使没有多线程。
答案 3 :(得分:4)
如果max
和in[]
的值远离64位最小值/最大值(例如,它们总是在-2 61 和+2 之间61 ),您可以尝试不带条件分支的循环,这可能会导致一些性能下降:
for(uint32_t i = 1; i < N; i++) {
max &= -max >> 63; // assuming >> would do arithmetic shift with sign extension
max += in[i];
out[i] = max;
}
理论上编译器也可以做类似的技巧,但是没有看到反汇编,很难判断它是否能够实现。
答案 4 :(得分:1)
代码显得非常快。根据数组的性质,您可以尝试使用特殊的套管,例如,如果您碰巧知道在特定的调用中所有输入数字都是正数,则[i]将等于累积总和,而不需要一个if分支。
答案 5 :(得分:1)
确保方法不是虚拟,内嵌, _ 属性 _((always_inline))和 -funroll-loops 似乎是值得探索的好选择。
只有通过对它们进行基准测试,我们才能确定它们是否值得在更大的程序中进行优化。
答案 6 :(得分:-1)
唯一想到可能帮助一小部分就是在循环中使用指针而不是数组索引的东西,比如
void theloop(int64_t in[], int64_t out[], size_t N)
{
int64_t max = in[0];
out[0] = max;
int64_t *ip = in + 1,*op = out+1;
for(uint32_t i = 1; i < N; i++) {
int64_t v = *ip;
ip++;
max += v;
if (v > max) max = v;
*op = max;
op++
}
}
这里的想法是,数组的索引很容易编译为获取数组的基址,将元素的大小乘以索引,并添加结果以获取元素地址。保持运行指针可以避免这种情况。我猜一个好的优化编译器已经这样做了,所以你需要研究当前的汇编程序输出。
答案 7 :(得分:-3)
int64_t max = 0, i;
for(i=N-1; i > 0; --i) /* Comparing with 0 is faster */
{
max = in[i] > 0 ? max+in[i] : in[i];
out[i] = max;
--i; /* Will reduce checking of i>=0 by N/2 times */
max = in[i] > 0 ? max+in[i] : in[i]; /* Reduce operations v=in[i], max+=v by N times */
out[i] = max;
}
if(0 == i) /* When N is odd */
{
max = in[i] > 0 ? max+in[i] : in[i];
out[i] = max;
}