【发布时间】:2017-04-29 00:09:22
【问题描述】:
我最近在 Code Review 上要求 a question 审查名为 QuickMergeSort 的排序算法。我不会详细介绍,但在某些时候,该算法会执行内部合并排序:它不是使用额外的内存来存储要合并的数据,而是将要合并的元素与来自原始序列的另一部分的元素交换,即'否则不会与合并有关。这是我关心的算法部分:执行合并的函数:
template<
typename InputIterator1,
typename InputIterator2,
typename OutputIterator,
typename Compare = std::less<>
>
auto half_inplace_merge(InputIterator1 first1, InputIterator1 last1,
InputIterator2 first2, InputIterator2 last2,
OutputIterator result, Compare compare={})
-> void
{
for (; first1 != last1; ++result) {
if (first2 == last2) {
std::swap_ranges(first1, last1, result);
return;
}
if (compare(*first2, *first1)) {
std::iter_swap(result, first2);
++first2;
} else {
std::iter_swap(result, first1);
++first1;
}
}
// first2 through last2 are already in the right spot
}
该函数改编自 std::inplace_merge 的 libc++ 实现中的同名函数;这个新版本将元素与原始数组的另一部分交换,而不是从辅助数组中移动元素。
由于合并是内部,我意识到我实际上不需要有两个单独的输入类型:InputIterator1 和InputIterator2 总是相同的。然后我意识到,由于first1和first2上的操作总是相同的,我可以将它们存储在一个二元素数组中,并使用比较的结果来索引数组以知道要交换哪个迭代器并增加。通过这个小技巧,我摆脱了分支并获得了一个几乎无分支的合并算法:
template<
typename InputIterator,
typename OutputIterator,
typename Compare = std::less<>
>
auto half_inplace_merge(InputIterator first1, InputIterator last1,
InputIterator first2, InputIterator last2,
OutputIterator result, Compare compare={})
-> void
{
InputIterator store[] = { first1, first2 };
for (; store[0] != last1; ++result) {
if (store[1] == last2) {
std::swap_ranges(store[0], last1, result);
return;
}
bool cmp = compare(*store[1], *store[0]);
std::iter_swap(result, store[cmp]);
++store[cmp];
}
// first2 through last2 are already in the right spot
}
现在,问题是:使用这个新的 half_inplace_merge 函数,整体排序算法比原来的 half_inplace_merge 慢 1.5 倍,我不知道为什么。我尝试了几个编译器优化级别,几个技巧来避免潜在的别名问题,但似乎问题来自无分支技巧本身。
那么,有没有人能解释一下为什么无分支代码比较慢?
附录: 对于那些想要运行与我相同的基准测试的人......好吧,这会有点困难:我使用了个人图书馆中的基准测试,其中包括很多东西;您需要下载the library,在某处添加this file,并在添加所需行以在突出显示部分附近调用quick_merge_sort 后运行this benchmark(您需要重定向程序的标准输出到profiles 子目录中的文件)。然后您需要运行this Python script 来查看结果,将quick_merge_sort 添加到突出显示的行。注意需要安装 NumPy 和 matplotlib。
【问题讨论】:
-
所有编译器都会出现这种情况吗? (我猜你已经检查过了,但我只是想做一个小的健全性检查。)
-
我想得越多,我就越怀疑访问任一数组元素所需的取消引用是问题所在。在原始代码中,编译器知道每种情况正在访问哪个迭代器,而在第二种情况下,内存访问无法优化。
-
查看汇编输出,我在第二个版本中看到了更复杂的间接寻址模式:godbolt.org/g/yjW1Ks - 并且没有更少的分支。
-
总结my comments here:您可能会将“预测”问题推入加载存储单元而不是分支预测器。由于地址的随机性,内存消歧器无法正确预测它们之间的依赖关系——从而得到与错误预测分支相同的惩罚。不幸的是,我没有办法检验这个理论。所以我把它作为评论。
-
能否请您提供一个包含可运行代码版本的 pastebin 链接?我可以为您的代码获取性能计数器值。
标签: c++ performance sorting branch-prediction