帮助编译器优化函数指针

20

在 C 语言中实现面向对象式的封装和多态常用的方法是返回一个包含函数指针的结构体的不透明指针。这种模式在 Linux 内核中非常频繁地出现。

使用函数指针而不是函数调用会引入一些开销,但由于缓存的存在,这种开销在大多数情况下可以忽略不计,就像其他问题已经讨论过的那样。

然而,随着 GCC (>4.6) 中新的 -fwhole-program 和 -flto 优化选项的出现,情况发生了变化。

libPointers.c

#include <stdlib.h>
#include "libPointers.h"

void do_work(struct worker *wrk, const int i) 
{
        wrk->datum += i;
}

struct worker *libPointers_init(const int startDatum)
{
        struct worker *wrk = malloc (sizeof (struct worker));

        *wrk = (struct worker) {
                .do_work = do_work,
                .datum = startDatum
        };

        return wrk;
}

libPointers.h

#ifndef __LIBPOINTERS_H__
#define __LIBPOINTERS_H__


struct worker {
        int datum;

        void (*do_work)(struct worker *, int i);
};

extern void do_work (struct worker *elab, const int i);

struct worker *libPointers_init(const int startDatum);


#endif //__LIBPOINTERS_H__

testPointers.c

#include <stdio.h>
#include "libPointers.h"


int main (void)
{
        unsigned long i;
        struct worker *wrk;

        wrk = libPointers_init(56);

        for (i = 0; i < 1e10; i++) {
#ifdef USE_POINTERS
                wrk->do_work(wrk,i);
#else
                do_work(wrk,i);
#endif
        }

        printf ("%d\n", wrk->datum);
}

使用-O3编译,但没有使用-flto和-fwhole-program标志,在我的机器上执行testPointers需要大约25秒的时间,无论是否定义了USE_POINTERS。

如果我打开-flto -fwhole-program 标志,同时定义了USE_POINTERS,testPointers需要大约25秒的时间,但是如果使用函数调用,需要大约14秒。

这是完全符合预期的行为,因为我理解编译器将在循环中内联和优化函数。然而,我想知道是否有一种方法可以帮助编译器告诉它函数指针是常量,从而允许它优化该情况。

对于使用cmake的人,这是我如何编译的

CMakeLists.txt

set (CMAKE_C_FLAGS "-O3 -fwhole-program -flto")
#set (CMAKE_C_FLAGS "-O3")
add_executable(testPointers
        libPointers.c
        testPointers.c
        )

1
如果你在循环外部捕获wrk->do_work的值并将其存入一个本地函数指针变量中,那么你可以在循环内使用该本地变量。 - Greg Hewgill
你怎么敢问一个好的、有意义的问题? :P (+1,这很有趣。) - user529758
1
一个问题是 do_work 做的工作非常少。如果它实际上做了一些重要的事情,调用速度的差异将更难以测量(因此不太显著)。 - Bo Persson
我理解,但getter/setter函数非常常见,而且它们甚至做的工作更少。通过整个程序的优化,我们有幸调节对数据的访问并在最终二进制文件中完全优化这些简单操作。 - Metiu
1
@GregHewgill 我这样改了,但是没有任何区别。 int main (void) { unsigned long i; struct worker *wrk; wrk = libPointers_init(56);#ifdef USE_POINTERS void (*const f_do_work)(struct worker *, int i) = wrk->do_work; #endif for (i = 0; i < 1e10; i++) {#ifdef USE_POINTERS f_do_work(wrk,i); #else do_work(wrk,i); #endif } printf ("%d\n", wrk->datum);} - Metiu
C++编译器可以进行虚函数调用内联的去虚拟化。但我认为让 C 编译器按照你的意愿执行这一过程会更加困难。 - Mysticial
2个回答

11

编译器无法内联函数,除非它能确定只会调用一个可能的版本。通过指针调用并不是显然的情况。如果你跟踪代码,可能仍然可以弄清楚,因为指针只有一个可能的值; 但是这超出了我对编译器的期望。


是的,我所说的“这是完全预期的行为”是指这一点。我在想是否有 const 属性的某种组合可以告诉编译器该函数不会发生更改,类似于“pure”、“const”等等……这至少可以帮助处理常见的“非虚拟方法”情况。 - Metiu
另一个常见的关键字是restrict关键字,它具有类似的约定:确保您可以以某种方式处理指针。能够说“这个回调将始终指向这个函数”会很好。 - Metiu
如果您作为程序员知道某种情况下将始终调用特定函数,则可以直接调用该函数而不是使用指针。 - Greg Hewgill

0
如果您正在循环调用函数指针,您可以将循环移动到函数指针内部。

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