在条件上更新variables的最快方法是什么?

我有一个指针, ptr和一个条件cond 。 如果condtrue ,则需要尽可能快的方法来重置ptr如果condtrue ,则保持ptr不变。 目前的实现是平凡的:

 void reset_if_true(void*& ptr, bool cond) { if (cond) ptr = nullptr; } 

我知道上面的代码的性能是好的,我不能指望一个主要的性能提升优化它。 然而,这个代码被称为每秒数百万次,每个小纳秒节省相关。

我在考虑摆脱这个分支的东西,例如:

 void* p[] = { ptr, nullptr }; ptr = p[cond]; 

但我不确定这是继续进行的最好方法。

 void reset_if_true(void*& ptr, bool cond) { if (cond) ptr = nullptr; } 

天真的解决scheme无疑是大多数情况下最快的解决scheme。 虽然它有一个分支,在现代stream水线处理器上可能会很慢,但是如果分支错误预测 ,分支只会很慢。 由于分支预测器现在非常好,除非cond的值是非常不可预测的,否则很可能是一个简单的条件分支是编写代码的最快方法。

如果不是这样,一个好的编译器应该知道,并且能够优化代码,以更好地考虑目标体系结构。 接下来要讲的就是 :只需简单的编写代码,然后将优化留在优化器手中。

虽然这是一般的好build议,但是有时候这太过分了。 如果你真的关心这个代码的速度,你需要检查一下编译器实际上在做什么。 检查它正在生成的目标代码,并确保它是明智的,并且该函数的代码正在被内联。

这样的考试可以相当揭示。 例如,让我们考虑一下x86-64,在分支预测被挫败的情况下(这实际上是唯一一次这是一个有趣的问题,所以让我们假设cond是完全不可预测的)分支可能是非常昂贵的。 几乎所有的编译器都会生成以下的简单实现:

 reset_if_true(void*&, bool): test sil, sil ; test 'cond' je CondIsFalse mov QWORD PTR [rdi], 0 ; set 'ptr' to nullptr, and fall through CondIsFalse: ret 

这就像你能想象的那样严密的代码。 但是,如果将分支预测器置于病态情况下,最终可能比使用条件移动慢。

 reset_if_true(void*&, bool): xor eax, eax ; pre-zero the register RAX test sil, sil ; test 'cond' cmove rax, QWORD PTR [rdi] ; if 'cond' is false, set the register RAX to 'ptr' mov QWORD PTR [rdi], rax ; set 'ptr' to the value in the register RAX ret ; (which is either 'ptr' or 0) 

有条件的移动具有相对较高的延迟,所以它们比预测好的分支慢得多,但它们可能比完全不可预知的分支要快。 你将会期望一个编译器在面向x86架构的时候知道这一点,但是它不会(至less在这个简单的例子中)对cond的可预测性有所了解。 它假定简单的情况,分支预测将在你身边,并生成代码A而不是代码B.

如果您决定鼓励编译器由于不可预知的情况而生成无分支代码,则可以尝试以下操作:

 void reset_if_true_alt(void*& ptr, bool cond) { ptr = (cond) ? nullptr : ptr; } 

这成功地说服Clang的现代版本生成无分支代码B,但在GCC和MSVC中是完全悲观的。 如果你没有检查生成的程序集,你不会知道这个。 如果你想强制GCC和MSVC生成无分支代码,你将不得不加倍努力。 例如,您可以使用问题中发布的变体:

 void reset_if_true(void*& ptr, bool cond) { void* p[] = { ptr, nullptr }; ptr = p[cond]; } 

当瞄准x86时,所有编译器都会为此生成无分支代码,但这不是特别漂亮的代码。 事实上,他们都没有产生有条件的举动。 相反,为了构build数组,您需要多次访问内存:

 reset_if_true_alt(void*&, bool): mov rax, QWORD PTR [rdi] movzx esi, sil mov QWORD PTR [rsp-16], 0 mov QWORD PTR [rsp-24], rax mov rax, QWORD PTR [rsp-24+rsi*8] mov QWORD PTR [rdi], rax ret 

丑陋,可能效率很低。 我预测,即使在分支被错误预测的情况下,它也会给出有条件的跳转版本。 当然,你必须对它进行基准testing,但这可能不是一个好的select。

如果你仍然想要消除MSVC或GCC上的分支,你必须做更丑陋的事情,包括重新解释指针位和旋转它们。 就像是:

 void reset_if_true_alt(void*& ptr, bool cond) { std::uintptr_t p = reinterpret_cast<std::uintptr_t&>(ptr); p &= -(!cond); ptr = reinterpret_cast<void*>(p); } 

这会给你以下几点:

 reset_if_true_alt(void*&, bool): xor eax, eax test sil, sil sete al neg eax cdqe and QWORD PTR [rdi], rax ret 

再一次,在这里我们得到了比简单分支更多的指令,但是至less它们是相对低延迟的指令。 一个现实的数据基准将告诉你,如果权衡是值得的。 如果你要真正签入这样的代码,给你的理由你需要在评论中。

一旦我下了这个有点麻烦的兔子洞,我就可以强制MSVC和GCC使用条件移动指令。 显然他们没有做这个优化,因为我们正在处理一个指针:

 void reset_if_true_alt(void*& ptr, bool cond) { std::uintptr_t p = reinterpret_cast<std::uintptr_t&>(ptr); ptr = reinterpret_cast<void*>(cond ? 0 : p); } 
 reset_if_true_alt(void*&, bool): mov rax, QWORD PTR [rdi] xor edx, edx test sil, sil cmovne rax, rdx mov QWORD PTR [rdi], rax ret 

考虑到CMOVNE的延迟和类似的指令数量,我不确定这是否会比以前的版本更快。 你跑的基准会告诉你是否是。

同样的,如果我们把这个条件交换一下,我们可以节省一个内存访问:

 void reset_if_true_alt(void*& ptr, bool cond) { std::uintptr_t c = (cond ? 0 : -1); reinterpret_cast<std::uintptr_t&>(ptr) &= c; } 
 reset_if_true_alt(void*&, bool): xor esi, 1 movzx esi, sil neg rsi and QWORD PTR [rdi], rsi ret 

(这就是GCC,MSVC做的稍微有点不同,它更喜欢它的negsbbnegdec指令的特征序列,但是这两者在道德上是等价的。Clang把它转换成我们在上面看到的同样的条件性的移动。如果我们需要避免分支,那么就是最好的代码,因为它会在所有testing的编译器上生成合理的输出,同时保持源代码的某些可读性。

这里最低的水果不是你想象的那样。 正如其他几个答案中所讨论的那样, reset_if_true将被编译为机器代码,这个代码的速度与您可以合理预期的一样快。 如果速度不够快,你需要开始考虑改变它的function 。 我在那看到两个select,一个简单,一个不那么容易:

  1. 更改调用约定:

     template <class T> inline T* reset_if_true(T* ptr, bool condition) { return condition ? nullptr : ptr; } 

    然后更改来电者阅读类似的东西

     ptr_var = reset_if_true(ptr_var, expression); 

    这样做会使得ptr_var更有可能在关键的最内层循环( reset_if_true调用reset_if_true数百万次)期间进入寄存器,并且不会有任何与之关联的内存访问。 在你的代码中, ptr_var被强制转移到内存中是最为昂贵的; 甚至比可能预测错误的分支更昂贵。 (一个足够好的编译器可以为你提供reset_if_true是可以的,但并不总是可以的。)

  2. 改变周围的algorithm,这样reset_if_true不会reset_if_true被调用数百万次。

    既然你没有告诉我们周围的algorithm是什么,我不能帮你。 但是,我可以告诉你,每秒钟做一些涉及检查条件数百万次的事情, 可能意味着一个二次时间复杂或更差的algorithm,这总是意味着你至less应该考虑find一个更好的algorithm。 (可能没有更好的,唉。)

只要我们有sizeof(size_t) == sizeof(void*) ,nullptr用二进制表示为0,size_t用所有位(或者有std :: uintptr_t),你可以这样做:

 // typedef std::uintptr_t ptrint_t; // uncomment if you have it typedef size_t ptrint_t; // comment out if you have std::uintptr_t void reset_if_true(void*& ptr, bool cond) { ((ptrint_t&)ptr) &= -ptrint_t( !cond ); } 

但是请注意,从boolsize_t所需的时间与实现有很大关系,并且可能需要一个分支。

代码是绝对简单的。

通过内联函数(如果编译器没有自行内联),你肯定会使事情变得更快。 例如,内联可能意味着您设置为null的指针variables可以保留在寄存器中。

除此之外,这段代码是非常直接的,如果有任何技巧可以使它更快,编译器会使用它们。

更新:我重新实现了我的答案。

在下面的代码中,这个想法是将指针转换成一个数字并且乘以一个数字(cond)。 注意inline使用。 乘法可能有助于使用使用stream水线的体系结构。

 #include <cstdint> template <typename T> inline T* reset_if_true(T* p, bool cond) { void* ptr = (void*)p; // The optimising compiler (-O3) will get rid of unnecessary variables. intptr_t ptrint; // This is an unrecommended practice. ptrint = (intptr_t)ptr; ptrint = ptrint * cond; // Multiply the integer void* ptr2 = (void*)ptrint; T* ptrv = (T*)ptr2; return ptrv; } 

用法示例:

 #include <iostream> #include <vector> void test1(){ //doulbe d = 3.141592; //typedef std::vector<double> mytype; std::vector<double> data = {3,1,4}; auto ptr = &data; std::cout << (void*)ptr << std::endl; auto ptr2 = reset_if_true(ptr, 1); //auto ptr2 = (mytype*)reset_if_true(ptr, 1); std::cout << reset_if_true(ptr, 1) << " -> " << (*(reset_if_true(ptr, 1))).size() << std::endl; std::cout << reset_if_true(ptr, 2) << " -> "<< (*(reset_if_true(ptr, 2))).size() << std::endl; std::cout << reset_if_true(ptr, 0) << " is null? " << (reset_if_true(ptr, 0) == NULL) << // Dont dereference a null. std::endl; } void test2(){ double data = 3.141500123; auto ptr = &data; std::cout << (void*)ptr << std::endl; auto ptr2 = reset_if_true(ptr, 1); //auto ptr2 = (mytype*)reset_if_true(ptr, 1); std::cout << reset_if_true(ptr, 1) << " -> " << (*(reset_if_true(ptr, 1))) << std::endl; std::cout << reset_if_true(ptr, 2) << " -> "<< (*(reset_if_true(ptr, 2))) << std::endl; std::cout << reset_if_true(ptr, 0) << " is null? " << (reset_if_true(ptr, 0) == NULL) << // Dont dereference a null. std::endl; } int main(){ test1(); test2(); } 

编译使用这些标志: -O3 -std=c++14 。 输出是:

 0x5690 0x5690 -> 3 0x5690 -> 3 0 is null? 1 0x5690 0x5690 -> 3.1415 0x5690 -> 3.1415 0 is null? 1 

在编译器命令行中使用这些选项时,可能会遇到内存alignment问题-s FORCE_ALIGNED_MEMORY=1 。 另请参阅reinterpret_cast 。 不要忘记使用-O3

cond可以是任何非零值。 如果我们知道它不是非0或1,那么在这里有一些性能改进的空间。在这种情况下,可以使用int另一个整数types作为cond。

PS。 这是一个更新的答案。 前面的答案,正如我在答复中已经明确提到的那样,有问题。 解决scheme是使用intptr_t ,当然是inline

使用的编译器选项:

  em++ reset_if_true.cpp -O3 -std=c++14 -o reset_if_true.js node reset_if_true.js