【问题标题】:Is it possible to micro-optimize "x = max(a,b); y = min(a,b);"?是否可以微优化“x = max(a,b); y = min(a,b);”?
【发布时间】:2015-05-20 15:21:15
【问题描述】:

我有一个算法,一开始就像

int sumLargest2 ( int * arr, size_t n )
{
    int largest(max(arr[0], arr[1])), secondLargest(min(arr[0],arr[1])); 
    // ... 

我意识到第一个可能不是最佳的,因为当您考虑到一旦找到最大值后,知道最小值所需的信息已经存在时,调用max 然后min 是重复的。所以我发现我可以做到

   int largest = max(arr[0], arr[1]);
   int secondLargest = arr[0] == largest ? arr[1] : arr[0];

减少对min 的无用调用,但我不确定这是否真的节省了任何数量的操作。是否有任何奇特的位移算法可以做相当于

int largest(max(arr[0], arr[1])), secondLargest(min(arr[0],arr[1]));

?????

【问题讨论】:

  • 您的基准测试显示这些“无用的 min 调用”有多大的瓶颈?
  • 首先,为什么此时需要这种优化?
  • 您非常怀疑您是否删除了一个无用的 min 调用,因为您的编译器无论如何都会内联它。您是否有任何分析结果表明它有所作为?您不会通过猜测正在执行哪些操作并进一步猜测每个操作需要多长时间来优化事物。您只需要测量它(不要忘记使用优化进行编译!)。查看生成的程序集,看看您是否有所作为。
  • @Borgleader 据我所知,我的整个程序没有瓶颈
  • 需要注意的一点是,我认为您正在以人类直观的方式查看执行时间,不幸的是它通常不是那么直观。例如,最小/最大通常转换为无分支汇编代码,只是在周围旋转比特,或者在最坏的情况下,是一个有条件的移动(它不会受到分支预测错误的影响)。在这里只使用 if/else 并避免冗余逻辑似乎更快,但您可能会用更昂贵的非冗余逻辑来换取非常便宜的冗余逻辑,这实际上会使事情变得更糟。

标签: c++ algorithm c++11 optimization bit-shift


【解决方案1】:

在 C++ 中,您可以使用std::minmax 生成最小值和最大值的std::pair。这与std::tie 结合起来特别容易:

#include <algorithm>
#include <utility>

int largest, secondLargest;
std::tie(secondLargest, largest) = std::minmax(arr[0], arr[1]);

至少,GCC 能够将 minmax 调用优化为单个比较,与下面的 C 代码的结果相同。

在 C 中,您可以自己编写测试:

int largest, secondLargest;
if (arr[0] < arr[1]) {
  largest = arr[1];
  secondLargest = arr[0];
} else {
  largest = arr[0];
  secondLargest = arr[1];
}

【讨论】:

    【解决方案2】:

    怎么样:

    int largestIndex = arr[1] > arr[0];
    int largest = arr[largestIndex];
    int secondLargest = arr[1 - largestIndex];
    

    第一行依赖于布尔结果的隐式转换,如果为真,则为 1,如果为假,则为 0。

    【讨论】:

    • 不错!总共有 1 次比较、1 次减法和 3 次分配。比我能想出的要好。
    • 没有跳转(编译器和处理器愿意),也没有函数调用。我希望您介绍这些潜在的解决方案中的每一个,我很想知道哪个对您来说实际上更快。
    【解决方案3】:

    我将假设您宁愿解决更大的问题...也就是说,获取数组中最大两个数字的总和。

    您正在尝试做的是std::partial_sort()。 让我们实现它。

    int sumLargest2(int * arr, size_t n) {
        int * first  = arr;
        int * middle = arr + 2;
        int * last   = arr + n;
    
        std::partial_sort(first, middle, last, std::greater<int>());
    
        return arr[0] + arr[1];
    }
    

    如果您无法修改arr,那么我建议您查看std::partial_sort_copy()

    【讨论】:

      【解决方案4】:
      x = max(a, b);
      y = a + b - x;
      

      不一定会更快,但会有所不同。

      还要小心溢出。

      【讨论】:

      • y = a ^ b ^ x 避免溢出。
      【解决方案5】:

      如果您的意图是减少函数调用以找到 min mad max,您可以尝试std::minmax_element。这从 C++11 开始可用。

      auto result = std::minmax_element(arr, arr+n);
      std::cout<< "min:"<< *result.first<<"\n";
      std::cout<< "max :" <<*result.second << "\n";
      

      【讨论】:

      • std::minmax_element 返回一对迭代器,而不是值。如果要输出结果,则需要取消引用它们。
      • std::minmax 确实返回了一对const T&amp;(我不知道你为什么刚才说“引用迭代器”)。您答案中的代码使用std::minmax_element,这是一个完全不同的功能。
      • @Blastfurnace 哦,很困惑,并更正了答案。感谢识别!!
      【解决方案6】:

      如果您只想找到两个值中较大的一个,请:

      if(a > b)
      {
          largest = a;
          second = b;
      }
      else
      {
           largest = b;
           second = a;
      }
      

      没有函数调用,一次比较,两次赋值。

      【讨论】:

        【解决方案7】:

        我假设 C++...

        简答,使用 std::minmax 并使用正确的优化和正确的指令集参数进行编译。

        冗长的丑陋答案,编译器无法做出所有必要的假设以使其真正非常快。你可以。在这种情况下,您可以更改算法以首先处理所有数据,然后可以强制对齐数据。做完这一切,你可以使用内在函数来加快速度。

        虽然我没有在这种特殊情况下对其进行测试,但我已经看到使用这些指南获得了巨大的性能改进。

        由于您没有将 2 个整数传递给函数,因此我假设您使用数组并希望以某种方式对其进行迭代。您现在可以选择:制作 2 个数组并使用 min/max 或使用 1 个数组同时使用 ab。仅此一项决定就已经可以影响性能。

        如果您有 2 个数组,则可以使用对齐的 malloc 在 32 字节边界上分配这些数组,然后使用内部函数进行处理。如果您要追求真正的原始性能 - 这就是您要走的路。

        F.ex,假设您有 AVX2。 (注意:我不确定你是否这样做,你应该使用 CPU id 检查这个!)。转到此处的备忘单:https://software.intel.com/sites/landingpage/IntrinsicsGuide/ 并选择你的毒药。

        您正在寻找的内在函数在这种情况下可能是:

        • _mm256_min_epi32
        • _mm256_max_epi32
        • _mm256_stream_load_si256

        如果您必须对整个数组执行此操作,您可能希望在合并各个项目之前将所有内容保存在单个 __mm256 寄存器中。例如:每个 256 位向量做一个最小/最大值,当循环完成时,提取 32 位项目并对其做一个最小值/最大值。

        更好的答案:所以......至于编译器。编译器确实尝试优化这些类型的东西,但遇到了问题。

        如果您要处理 2 个不同的数组,编译器必须知道它们是不同的,才能对其进行优化。这就是为什么 restrict 这样的东西存在的原因,它告诉编译器这个你在编写代码时可能已经知道的小东西。

        此外,编译器不知道您的内存已对齐,因此它必须检查这一点并为每个调用分支...。我们不想要这个;这意味着我们希望它内联它的东西。所以,添加inline,把它放在一个头文件中,就是这样。也可以使用aligned给他提示。

        您的编译器也没有得到int* 不会随时间改变的提示。如果无法更改,最好告诉他使用 const 关键字。

        编译器使用指令集进行编译。通常,他们已经使用 SSE,但 AVX2 可以提供很多帮助(正如我在上面的内在函数中所展示的那样)。如果您可以使用这些标志编译它,请确保使用它们 - 它们有很大帮助。

        在发布模式下运行,在“快速”上使用优化进行编译,看看幕后会发生什么。如果您执行所有这些操作,您应该会看到 vpmax... 指令出现在内部循环中,这意味着编译器可以很好地使用内部函数。

        我不知道你还想在循环中做什么......如果你使用所有这些指令,你应该在大数组上达到内存速度。

        【讨论】:

          【解决方案8】:

          时间和空间的权衡怎么样?

          #include <utility>
          
          template<typename T>
              std::pair<T, T>
                  minmax(T const& a, T const& b)
                  { return b < a ? std::make_pair(b, a) : std::make_pair(a, b); }
          
          //main
          std::pair<int, int> values = minmax(a[0], a[1]);
          int largest       = values.second;
          int secondLargest = values.first;
          

          【讨论】:

          • 已经存在一个std::minmax,看起来和你写的很相似。
          • 是的。 OP 标记了 C++,而不是 C++11。
          猜你喜欢
          • 2021-09-21
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-03-29
          • 1970-01-01
          相关资源
          最近更新 更多