【问题标题】:C branch on static variable optimization静态变量优化的 C 分支
【发布时间】:2011-09-07 11:03:35
【问题描述】:

让我先说我没有分析此代码,也不是关键路径。这主要是出于我自己的好奇心。

我有一个函数将静态 int 声明/定义为已知错误值,这将导致代码采用分支。但是,如果函数成功,我可以肯定地知道该分支将永远不会再被采用。是否有编译时优化?特别是 GNU/gcc/glibc?

所以我有这个:

static unsigned long volatile *getReg(unsigned long addr){

    static int fd = -1;

    if (fd < 0){
        if (fd = open("file", O_RDWR | O_SYNC) < 0){
            return NULL;
        }
    }
}

因此,一旦函数成功完成(如果此函数返回 null,我将退出程序),我知道 fd 对于所有未来的调用都是有效的,并且永远不会采用第一个分支。我知道有 __builtin_expect() 宏,所以我可以写

if (__builtin_expect((fd<0),0){

但据我了解,这只是对编译器的提示,它仍然必须执行条件检查。而且我还意识到,在 99.9999% 的情况下,这已经绰绰有余,因此任何进一步的性能提升都可以忽略不计。

我想知道是否有办法在第一次运行后阻止第一次条件检查(fd

【问题讨论】:

  • 缺少自修改代码,我不相信有办法在您期望行为因条件而异的情况下避免条件(或等效的函数指针)!

标签: c optimization branch control-flow


【解决方案1】:

简短的回答是“不”。

我的意思是,当然,您可以使用指向函数的指针、猴子修补您的代码等来耍花招,但这几乎肯定会比仅仅进行测试要慢。

分支只有在预测错误时才会变得昂贵。 __builtin_expect 将安排确保此分支仅在第一次被错误预测。

您在这里实际上是在谈论一两个周期,甚至可能不是,这取决于 CPU 在此代码附近还做了什么。

[更新]

如果这样的事情确实每秒被调用数百万或数十亿次,您可以通过重组代码来处理它以尽早初始化fd,然后重复使用它而无需费心进行测试。例如,您可以在 main() 顶部附近添加一个 initGlobalState(); 调用,然后打开文件。 (您需要相应的destroyGlobalState(); 再次关闭它。)

当然,文件描述符是一个可怕的例子,因为无论如何你对它做的任何事情都将花费一两个周期以上。

顺便说一句,在 C++ 中,构造函数、析构函数和 RAII idiom 使这种方法非常自然。

【讨论】:

  • +1:同意。当然,在某些情况下,如果函数每秒被调用数百万次,那么几个周期可能很重要。
  • 我同意你们俩。条件检查无关紧要。但就像我说的,这纯粹是为了教育和实习目的 =]
  • @Oli:除了函数调用本身需要更长的时间......但我想我会添加一个更新。谢谢。
  • @Nemo:除非它是内联的!
  • @Nemo:有时如果函数调用需要更少的时间,内联仍然更好。事实上,编译器不会内联,而是将代码克隆并放置在调用者附近,这反过来可以避免大量内存在主内存和缓存之间来回传输。这取决于。
【解决方案2】:

将函数一分为二,在它们自己的源文件中...让调用者担心它:)

static int fd;

unsigned long volatile *getReg(unsigned long addr) {
  /* do stuff with fd and addr */
  return 0;
}

int getRegSetup(void) {
  fd = open("file", O_RDWR | O_SYNC);
  if (fd < 0) return 1;                /* error */
  /* continue processing */
  return 0;                            /* ok */
}

然后调用者会这样做

  /* ... */
  if (getRegSetup()) {
    /* error */
  } else {
    do {
      ptr = getReg(42);
    } while (ptr);
  }
  /* ... */

【讨论】:

  • 但这并没有消除条件检查。它只是把它移到别处。而且现在它的几条指令更长了,因为它必须查找函数地址等。
  • 条件只测试一次,即使你需要调用getReg一百万次
  • @Falmarri,它确实在所有对 getReg 的调用中消除它。
  • @paxdiablo:但是您刚刚将条件转移到检查 getRegSetup() 而不是检查 fd
  • @Falmarri:我做了一些假设来说明代码的工作原理。不管是循环还是中断……只是移动代码。
【解决方案3】:

解决这个问题的方法之一是使用函数指针来调用该方法。将函数 ptr 初始化为您的长函数,并在第一次调用结束时将其设置为版本,无需额外初始化。

也就是说,这听起来像是一场绝对的维护噩梦,当然不值得避免一个分支 - 但你摆脱了分支..(当然也摆脱了函数被内联的任何机会,这取决于多长时间该功能几乎肯定是有害的)

【讨论】:

  • 我接受这个答案,因为它实际上听起来是最准确的答案。我没有考虑切换函数指针。
  • 它也几乎肯定会比简单实现,因为间接调用比直接调用慢(可能慢数百倍)。所以这更难实现,更难阅读,更慢......但除此之外,这是一个完美的答案。 (顺便提一下,当我提到“函数指针的技巧”时,我指的是这个。)
  • 是的。我理解它会变慢的事实。我的问题不是性能优化。我在想,如果条件检查本身有你不想要的副作用怎么办?或者如果在错误条件之后运行检查本身可能会导致错误? (我知道,重构检查/状态,显然 =P)
【解决方案4】:

__builtin_expect 只是一个提示。它有助于编译器生成更好的代码。例如,重新排列跳转标签,使主线代码在内存中不断对齐,这对代码缓存行更友好,更容易从主内存中获取等。运行配置文件引导优化更好。

我没有在您的代码中看到任何锁定,因此我假设不应同时从多个线程调用此函数。在这种情况下,您必须将fd 移出函数范围,以便不应用双重检查锁定。然后,重新安排一下代码(这就是 GCC 应该对分支提示做的事情,但你知道......)。另外,如果您经常访问它,您可以将文件描述符从主内存/缓存行复制到寄存器中。代码将如下所示:

static int g_fd = -1;

static unsigned long volatile *getReg(unsigned long addr)
{
    register int fd = g_fd;

    if (__builtin_expect ((fd > 0), 1))
    {
on_success:
        return NULL; // Do important stuff here.
    }

    fd = open("file", O_RDWR | O_SYNC);

    if (__builtin_expect ((fd > 0), 1))
    {
        g_fd = fd;
        goto on_success;
    }

    return NULL;
}

但是请不要把这当回事。系统调用和文件 I/O 太糟糕了,所以优化这样的东西没有任何意义(有一些例外)。

如果你真的想调用一次,那么你最好将文件打开移动到一个单独的函数中,该函数被调用一次,然后再调用其他所有内容。是的,看看 GCC 的个人资料反馈和 LTO。这将帮助您获得良好的结果,而无需在此类事情上花费太多时间。

【讨论】:

  • __builtin_expect 不仅如此;它实际上有助于编译器的分支预测。默认分支预测是假设不采用前向分支而采用后向分支。 (这可以在运行时动态变化,因为 CPU 会跟踪每个分支的执行频率。)如果您使用不同的 __builtin_expect 设置编译(例如)x86_64 并查看程序集,您会发现这正是它的样子做(即假设前向分支=未采用,后向分支=采用)。在某些架构上,__builtin_expect 实际上在分支 insn 中设置了位
  • @Nemo __builtin_expect 在 x86 上设置谓词吗?不知道这些对现代 CPU 是否有用,但听起来它可以做到。
  • @Voo:x86 在分支指令中没有预测位,但是 __builtin_expect 会导致 GCC 重新排列生成的代码以匹配 CPU 的默认预测启发式。
【解决方案5】:

对于任何好奇的人,这就是我想出的。请注意,这是一个更大、长时间运行程序的模块。此外,它还没有经过审查,而且基本上是一个糟糕的 hack。

__attribute__((noinline)) static unsigned int volatile *get_mem(unsigned int addr) {
    static void *map = 0 ;
    static unsigned prevPage = -1U ;
    static int fd = -1;
    int poss_err = 0;
    register unsigned page = addr & ~MAP_MASK ;

    if ( unlikely(fd < 0) ) {
        if ((fd = open("/dev/mem", O_RDWR | O_SYNC)) < 0) {
            longjmp(mem_err, errno);
        }
    }
    if ( page != prevPage ) {
        if ( map ) {
            if (unlikely((munmap(map,MAP_SIZE) < 0))) poss_err = 1;
        }
        if (unlikely((map = mmap(0, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, page )) == MAP_FAILED)) longjmp(mem_err, errno);

        prevPage = page ;
    }
    return (unsigned int volatile *)((char *)map+(addr & MAP_MASK));
}

static void set_reg(const struct reg_info * const r, unsigned int val)
{
    unsigned int volatile * const mem = get_mem(r->addr);
    *mem = (*mem & (~(r->mask << r->shift))) | (val << r->shift);
}

// This isn't in the final piece. There are several entry points into this module. Just an example

static int entryPoint(unsigned int value){

    if (setjmp(mem_err)!=0) {
        // Serious error
        return -1;
    }

    for (i=0; i<n; i++) {
        if (strlen(regs[i].name) == strlen(name) &&
                strncmp(regs[i].name, name, strlen (name))==0) {

            set_reg(&regs[i], value);
            return value;
        }
    }
}

这显然不是问题的答案,因为它会检查每次调用的条件。

【讨论】:

    猜你喜欢
    • 2013-09-04
    • 2011-01-29
    • 1970-01-01
    • 1970-01-01
    • 2021-08-19
    • 2011-04-19
    • 2012-10-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多