优化循环与代码复制

Rot*_*tem 5 c++ optimization loops

我的困境是如何最好地处理可以接受参数的长重循环.请考虑以下方法:

void HeavyLoop(byte* startingAddress, bool secondaryModification)
{
    for (int i = 0; i < 10000000; i++)
    {
        byte* b = startingAddress + i;
        *b+= 1;
        if (secondaryModification) *b+= 2;
    }
}
Run Code Online (Sandbox Code Playgroud)

这个方法会做我想要的,但是我if在循环中使用了10000000个不必要的s.

我是否写过这样的方法:

void HeavyLoop(byte* startingAddress, bool secondaryModification)
{
    if (secondaryModification)
    {
        for (int i = 0; i < 10000000; i++)
        {
            byte* b = startingAddress + i;
            *b+= 1;
            *b+= 2;
        }
    }
    else
    {
        for (int i = 0; i < 10000000; i++)
        {
            byte* b = startingAddress + i;
            *b+= 1;         
        }
    }   
}
Run Code Online (Sandbox Code Playgroud)

虽然我的整个循环代码必须重复,但我会得到相同的结果.如果我们谈论一个参数,这不是什么大问题,但是当你有4个独立参数时,我将不得不编写16个不同版本的循环.

在这种情况下,"正确"的解决方案是什么?如果这是像Python这样的语言,我可以动态地构建一个函数来处理循环.C++中有类似的东西吗?

不用说,代码只是一个例子而不是实际情况.请不要提供与*b+=1本身有关的解决方案. 我是C++新手,请原谅我,如果有一个我不知道的简单解决方案.如果有语法错误,请原谅我,目前我还没有编译器.

编辑:问题是处理无法在循环外预先计算的语句.

Mik*_*our 15

您可以将循环实现为模板; 模板参数是一个编译时常量,所以优化应该在它不正确时删除不需要的代码.然后,您需要一个包装器,以允许根据运行时值调用正确的特化:

template <bool secondaryModification>
void HeavyLoop(byte* startingAddress)
{
    for (int i = 0; i < 10000000; i++)
    {
        byte* b = startingAddress + i;
        *b+= 1;
        if (secondaryModification) *b+= 2;
    }
}

void HeavyLoop(byte* startingAddress, bool secondaryModification)
{
    if (secondaryModification) {
        HeavyLoop<true>(startingAddress);
    } else {
        HeavyLoop<false>(startingAddress);
    }
}
Run Code Online (Sandbox Code Playgroud)

在编译期间,将实例化模板的两个版本(一个包含*b+=2;一个版本,一个不包含,并且不对参数执行运行时测试); 然后应该在包装函数中内联它们以生成与第二个示例完全相同的代码 - 但不需要复制任何源代码.


Mat*_* M. 11

编辑:为了更好地瞄准OP想要的东西,仍然删除了这个帖子,这篇文章已经过彻底的编辑.

当然,我会假设您已对此进行了描述,这显示为热点......对吗?

事实上,我打赌你没有.并且你严重低估了你的编译器.

例如,这是使用LLVM编译的代码:

void f1(char*);
void f2(char*);

void loop(char* c, int n, int sm) {
  for (int i = 0; i < n; ++i) {
    if (sm) f1(c);
    else f2(c);
  }
}
Run Code Online (Sandbox Code Playgroud)

产量:

define void @loop(i8* %c, i32 %n, i32 %sm) nounwind uwtable {
  %1 = icmp sgt i32 %n, 0
  br i1 %1, label %.lr.ph, label %._crit_edge

.lr.ph:                                           ; preds = %0
  %2 = icmp eq i32 %sm, 0
  br i1 %2, label %3, label %5

; <label>:3                                       ; preds = %3, %.lr.ph
  %i.01.us = phi i32 [ %4, %3 ], [ 0, %.lr.ph ]
  tail call void @f2(i8* %c) nounwind
  %4 = add nsw i32 %i.01.us, 1
  %exitcond = icmp eq i32 %4, %n
  br i1 %exitcond, label %._crit_edge, label %3

; <label>:5                                       ; preds = %5, %.lr.ph
  %i.01 = phi i32 [ %6, %5 ], [ 0, %.lr.ph ]
  tail call void @f1(i8* %c) nounwind
  %6 = add nsw i32 %i.01, 1
  %exitcond2 = icmp eq i32 %6, %n
  br i1 %exitcond2, label %._crit_edge, label %5

._crit_edge:                                      ; preds = %5, %3, %0
  ret void
}
Run Code Online (Sandbox Code Playgroud)

即使您不知道LLVM IR,也只需遵循"sm"变量:

.lr.ph:                                           ; preds = %0
  %2 = icmp eq i32 %sm, 0
  br i1 %2, label %3, label %5
Run Code Online (Sandbox Code Playgroud)

编译器生成了两个不同的循环(分别从<label>:3和开始<label>:5,并选择一次,并且所有循环在fonction开始时执行).

这是一个众所周知的编译器技巧:循环不变代码运动(并衍生),所以为什么还要手动做呢?如果它值得,编译器就会这样做!

  • @Matthieu:我测试了这种方法(没有手动优化)与 Mike 建议的模板函数。测试函数采用 5 个布尔参数。当所有参数都传递为 true 时,或者换句话说,当不需要丢弃任何代码分支时,性能没有区别。然而,当它们都为假时,我确实得到了约 35% 的性能差异。但绝对有可能,由于缺乏经验,我没有正确设置一些编译器开关。 (2认同)

Pau*_*l R 9

在C和C++中都有这种问题的一种技术是使用内联函数.对于C++,您只能使用模板功能(实际上是相同的解决方案,但稍微更优雅).

这是C/C++的内联解决方案:

inline void HeavyLoop_inline(byte* startingAddress, bool secondaryModification)
{
    for (int i = 0; i < 10000000; i++)
    {
        byte* b = startingAddress+ i;
        *b+= 1;
        if (secondaryModification) *b+= 2;
    }
}

void HeavyLoop(byte* startingAddress, bool secondaryModification)
{
    if (secondaryModification)
    {
        HeavyLoop_inline(startingAddress, TRUE);
    }
    else
    {
        HeavyLoop_inline(startingAddress, FALSE);
    }
}
Run Code Online (Sandbox Code Playgroud)

它工作(并且有效)的原因secondaryModification是传递给内联函数的值是编译时常量,因此编译器能够优化每个调用的任何死代码.然后,这将为您提供两个"专用"版本的功能.


笔记

根据您使用的编译器,您可能需要采取其他步骤以确保内联函数实际内联.例如,你可以添加gcc __attribute__ ((always_inline)).

另请注意,某些编译器将在不进行任何干预的情况下执行此类循环重构因子优化,因此请在尝试优于编译器之前先检查生成的代码.

  • +1只要函数实际内联,这应该具有所需的效果; 该标志是编译时常量,因此优化器应该能够删除死代码.我可能会使用模板参数,以确保编译器将其视为编译时常量(正如我在回答之前所做的那样). (3认同)
  • +1,那些不理解这一点的人应该用适当的优化来编译和反汇编它.内联允许优化器静态预测分支`if(secondaryModification)`,因此在FALSE情况下它将被完全消除,并且在真实情况下,条件的测试将被消除并且`*b + = 2`保留在然后希望结合`*b + = 1`. (3认同)
  • +编译器应该能够告诉参数是已知的,因此它可以在编译时包含或排除`*b + = 2`. (2认同)