C/C++ 中的自展开宏循环

Posted

技术标签:

【中文标题】C/C++ 中的自展开宏循环【英文标题】:Self-unrolling macro loop in C/C++ 【发布时间】:2015-03-29 16:24:57 【问题描述】:

我目前正在开展一个项目,每个周期都很重要。在分析我的应用程序时,我发现一些内部循环的开销非常高,因为它们只包含一些机器指令。此外,这些循环中的迭代次数在编译时是已知的。

因此,我认为与其通过复制和粘贴手动展开循环,不如使用宏在编译时展开循环,以便以后轻松修改。

我的形象是这样的:

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

这样我就可以将for (int i = 0; i &lt; 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 的修改版本来构建我正在开发的架构。所以我认为技术上是的。 你看过-funroll-loops吗? 无论我配置它做什么,编译器都不会展开这个循环。旁注:我一直想知道如何将其用于教育目的,而不仅仅是针对这种特定情况。 为什么不能为此使用 Boost?如果是出于技术原因(这似乎不太可能),那么我怀疑您是否可以做到这一点。毕竟,如果我理解正确的话,Boost PP 只是标题库。如果没有别的,你应该可以从 Boost 中看看它是如何自己完成的。 @user694733:我不能使用 Boost,因为项目不能有任何依赖项。我查看了BOOST_PP_REPEAT 的源代码,它似乎与大多数提议的解决方案大致相同。我希望有一个更通用的解决方案,但我想这是不可能的,因为你不能写递归宏...... 【参考方案1】:

您可以使用模板展开。 查看示例的反汇编Live on Godbolt

但是-funroll-loops has the same effect for this sample。


Live On 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 具有相同的能力(取决于代码) 绝对是一种有趣的方法,但我怀疑我的编译器是否支持这一点。我会在我尝试后报告。 嗯,性能至关重要,足以陷入宏 hack,但对于使用较新的编译器编译一个源代码来说,性能还不够重要。 @user3629249:我知道大多数处理器(尤其是 x86)都是如此,但我正在使用的处理器是没有分支预测的特殊 ASIP。因此,每次迭代至少需要 3 个额外的周期(增量、分支和 1 个管道因分支而停止)。如果在我的情况下,循环只包含 1 到 8 条指令,这会相当慢。 @Potatoswatter:我不能使用更新的编译器,因为我的处理器没有。【参考方案2】:

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

#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();)

【讨论】:

THREECOUNT 有什么好处? @harper:如果您有许多循环以这种方式展开并且都使用相同的重复计数,那么您可以有一个全局定义。当你调整你的表现时,你只需要在一个地方改变计数。那个地方甚至可能是外部的,即编译器选项或 Makefile。但主要是我已经合并了它,因为 OP 在他的示例中使用了这种设置。 啊,我明白了。它与特定的宏无关,但应始终减少数字文字的使用并用符号替换它们。 您使用了许多扩展。两个扩展就足够了:#define M_REPEAT_(N, X) M_REPEAT ## N(X) + #define M_REPEAT(N, X) M_REPEAT_(N, X) 如果你想使用另一个宏作为计数器。【参考方案3】:

没有标准的方法。

这是一个有点疯狂的方法:

#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;

【讨论】:

【参考方案4】:

您不能使用#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 测试

【讨论】:

【参考方案5】:

你不能用宏编写真正的递归语句,我很确定你也不能在宏中进行真正的迭代。

不过你可以看看Order。尽管它完全构建在 C 预处理器之上,但它“实现”了类似迭代的功能。它实际上可以进行多达 N 次迭代,其中 N 是一个很大的数字。我猜它与“递归”宏类似。无论如何,这是一个边缘情况,很少有编译器支持它(不过 GCC 就是其中之一)。

【讨论】:

以上是关于C/C++ 中的自展开宏循环的主要内容,如果未能解决你的问题,请参考以下文章

循环展开有利的条件以及收益率下降的点?

C语言中,宏替换的替换规则

C语言编译系统对宏替换的处理是在啥时候进行的

define

在带引号的字符串中展开宏[重复]

宏在电脑中是啥意思起啥作用?