C/C++中的自解卷宏循环

19

我目前正在进行一个项目,每个循环都非常重要。在分析我的应用程序时,我发现某些内部循环的开销非常高,因为它们只由几条机器指令组成。此外,这些循环中的迭代次数在编译时已知。

所以我想,与其手动复制和粘贴展开循环,不如使用宏在编译时展开循环,这样可以轻松修改。

我想象的是这样的:

#define LOOP_N_TIMES(N, CODE) <insert magic here>

为了让我可以用以下代码替换 for (int i = 0; i < N, ++i) { do_stuff(); }:

#define INNER_LOOP_COUNT 4
LOOP_N_TIMES(INNER_LOOP_COUNT, do_stuff();)

它会展开成:

do_stuff(); do_stuff(); do_stuff(); do_stuff();

由于C预处理器对我来说大部分时间还是个谜,所以我不知道如何完成这个任务,但我知道一定是可以完成的,因为Boost似乎有一个BOOST_PP_REPEAT宏。不幸的是,我不能在这个项目中使用Boost。


我正在使用修改过的GCC版本来处理我所工作的架构。因此,从技术上讲,是的。 - Karsten
5
你看过-funroll-loops了吗? - dmg
2
编译器无论我如何配置,都不会展开此循环。顺便说一句:出于教育目的,我总是想知道如何做到这一点,而不仅仅是针对这个特定情况。 - Karsten
为什么你不能使用Boost来做这件事?如果是出于技术原因(这似乎不太可能),那么我怀疑你根本无法完成这个任务。毕竟,Boost PP是一个仅包含头文件的库,如果我理解正确的话。如果没有其他办法,你应该能够从Boost中学习如何自己完成它。 - user694733
2
@user694733:我不能使用Boost,因为项目不能有任何依赖。我查看了BOOST_PP_REPEAT的源代码,它似乎与大多数提出的解决方案差不多。我希望有一个更通用的解决方案,但我想这是不可能的,因为你不能编写递归宏... - Karsten
可能是在C预处理器中编写while循环的重复问题。 - Cristian Ciupitu
5个回答

31
您可以使用模板进行展开。请查看示例的反汇编代码 在 Godbolt 上实时查看

enter image description here

但是,对于此示例,-funroll-loops 具有相同的效果

在 Coliru 上实时查看

template <unsigned N> struct faux_unroll {
    template <typename F> static void call(F const& f) {
        f();
        faux_unroll<N-1>::call(f);
    }
};

template <> struct faux_unroll<0u> {
    template <typename F> static void call(F const&) {}
};

#include <iostream>
#include <cstdlib>

int main() {
    srand(time(0));

    double r = 0;
    faux_unroll<10>::call([&] { r += 1.0/rand(); });

    std::cout << r;
}

展示如何在不使用邪恶的宏的情况下完成此操作。并展示-funroll-loops也可以实现相同的效果(取决于代码)。 - sehe
这绝对是一个有趣的方法,但我怀疑我的编译器是否支持它。我会在尝试后回报。 - Karsten
3
性能对于使用宏技巧是至关重要的,但不至于连一个源文件都不能用更高版本的编译器进行编译。 - Potatoswatter
3
我了解大多数处理器(尤其是x86)都是如此,但我正在使用的处理器是一种特殊的ASIP,没有分支预测功能。因此,每次迭代都需要至少3个额外周期(增量、分支和1个由于分支而导致的流水线停顿)。如果像我这样的情况下循环仅包含1到8条指令,那么这会导致相当大的减速。 - Karsten
3
我无法使用更新的编译器,因为没有适用于我的处理器的更新版本。 - Karsten
显示剩余7条评论

16

您可以使用预处理器,并通过标记连接和多个宏扩展来进行一些技巧,但您必须硬编码所有可能性:

#define M_REPEAT_1(X) X
#define M_REPEAT_2(X) X X
#define M_REPEAT_3(X) X X X
#define M_REPEAT_4(X) X X X X
#define M_REPEAT_5(X) X M_REPEAT_4(X)
#define M_REPEAT_6(X) M_REPEAT_3(X) M_REPEAT_3(X)

#define M_EXPAND(...) __VA_ARGS__

#define M_REPEAT__(N, X) M_EXPAND(M_REPEAT_ ## N)(X)
#define M_REPEAT_(N, X) M_REPEAT__(N, X)
#define M_REPEAT(N, X) M_REPEAT_(M_EXPAND(N), X)

然后像这样扩展它:

#define THREE 3

M_REPEAT(THREE, three();)
M_REPEAT(4, four();)
M_REPEAT(5, five();)
M_REPEAT(6, six();)

这种方法需要用字面数字作为计数,你不能像这样做:

#define COUNT (N + 1)

M_REPEAT(COUNT, stuff();)

2
THREECOUNT的好处是什么? - harper
1
@harper:如果你有很多需要展开的循环,而且它们都使用相同的重复次数,你可以有一个全局定义。当你调整性能时,你只需要在一个地方更改计数。那个地方甚至可以是外部的,即编译器选项或Makefile。但主要是因为OP在他的例子中使用了这种设置,所以我将其合并进来了。 - M Oehm
啊,我明白了。这与特定的宏无关,但是我们应该始终减少使用数字字面量,并用符号替换它们。 - harper
你使用了许多扩展。如果你想要使用另一个宏作为计数器,那么两个扩展就足够了:#define M_REPEAT_(N, X) M_REPEAT ## N(X) + #define M_REPEAT(N, X) M_REPEAT_(N, X) - Knut

12

没有标准的方式来实现这个。

这里有一种略微疯狂的方法:

#define DO_THING printf("Shake it, Baby\n")
#define DO_THING_2 DO_THING; DO_THING
#define DO_THING_4 DO_THING_2; DO_THING_2
#define DO_THING_8 DO_THING_4; DO_THING_4
#define DO_THING_16 DO_THING_8; DO_THING_8
//And so on. Max loop size increases exponentially. But so does code size if you use them. 

void do_thing_25_times(void){
    //Binary for 25 is 11001
    DO_THING_16;//ONE
    DO_THING_8;//ONE
    //ZERO
    //ZERO
    DO_THING;//ONE
}

要求优化器消除死代码并不过分。

在这种情况下:

#define DO_THING_N(N) if(((N)&1)!=0){DO_THING;}\
    if(((N)&2)!=0){DO_THING_2;}\
    if(((N)&4)!=0){DO_THING_4;}\
    if(((N)&8)!=0){DO_THING_8;}\
    if(((N)&16)!=0){DO_THING_16;}

3
您不能使用 #define 结构来计算“展开次数”。但是,通过足够的宏,您可以定义如下内容:
#define LOOP1(a) a
#define LOOP2(a) a LOOP1(a)
#define LOOP3(a) a LOOP2(a)

#define LOOPN(n,a) LOOP##n(a)

int main(void)
{
    LOOPN(3,printf("hello,world"););
}

已使用VC2012进行测试


0

你不能使用宏编写真正的递归语句,我相信你也不能在宏中实现真正的迭代。

但是你可以看一下Order。虽然它完全建立在C预处理器之上,但它“实现”了类似迭代的功能。它实际上可以有最多N次迭代,其中N是一个很大的数字。我猜递归宏也是类似的。无论如何,这是一个边缘案例,很少有编译器支持它(GCC是其中之一)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接