4

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

我有一个函数将静态 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 <0 )。

4

5 回答 5

4

最简洁的答案是不”。

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

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

您在这里实际上是在谈论一两个周期,甚至可能不是,这取决于 CPU 在此代码附近执行的其他操作。

[更新]

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

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

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

于 2011-06-02T23:20:56.813 回答
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);
  }
  /* ... */
于 2011-06-02T23:20:43.797 回答
1

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

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

于 2011-06-03T17:22:05.193 回答
0

__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。这将帮助您获得良好的结果,而无需在此类事情上花费太多时间。

于 2011-06-02T23:42:08.723 回答
0

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

__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;
        }
    }
}

这显然不是问题的答案,因为它会检查每个呼叫的条件。

于 2011-06-22T18:10:50.767 回答