我们正在尝试重构解释器循环SWI-Prolog。 这是一个使用GCC的标签地址来引用虚拟机指令的巨大功能。 此函数具有许多 VM 寄存器变量。 事实证明,一个人写不写是有区别的
PL_next_solution(qid_t qid)
{ type1 v1;
type2 v2;
// 13 in total
// lots of code
}
或
PL_next_solution(qid_t qid)
{ struct
{ type1 v1;
type2 v2;
// 13 in total
} registers;
// lots of code
}
如果没有配置文件引导优化,结果程序的性能差异很小(<5%),而使用PGO优化,差异约为20%。 这是相当出乎意料的。 为什么会这样? gcc 是否尊重结构的内存布局,尽管它在此函数之外是未知的?
根据您的进一步测试,听起来性能影响来自获取一个或多个结构成员的地址并将其传递给编译器看不到的函数。 我相信当你这样做时,编译器必须假设整个结构已经转义。 这抑制了许多可能的优化:这意味着结构必须在内存中布局,此外,成员必须在每次函数调用前后存储到该内存并从中重新加载。
要了解原因,请看如下示例:
void foo(int *);
void bar(void);
struct qux {
int a,b,c;
char huge[5000];
};
int fum(void) {
struct qux s = { 1,2,3 };
foo(&s.b);
s.c=4;
bar();
return s.a+s.c;
}
试穿神螺栓
请注意,即使进行了最大程度的优化:
- 未使用的成员
huge
仍被分配和初始化 s.c=4
涉及内存存储s.a
和s.c
在调用bar()
后从内存中重新加载
我认为关键是,对于编译器所知,foo()
和bar()
可以执行的函数:
int *global_ptr;
void foo(int *ip) {
struct qux *qp = (struct qux *)((char *)ip - offsetof(struct qux, b));
global_ptr = &qp->c;
printf("%d %dn", qp->a, qp->huge[1234]);
}
void bar(void) {
*global_ptr = 37;
}
我相信这样的代码会定义得很好;foo
必须打印出1 0
,fum
必须返回 38。 当然,程序员需要确保只用struct qux
的b
成员的地址调用foo
,并且在fum
返回后不会再次调用bar()
,因为指针悬空 - 但如果他们这样做,那么代码应该可以工作。
另一方面,如果您将fum
中的调用更改为foo(NULL)
,您可以看到一切都消失了:s.huge
从未初始化甚至分配,s.b
完全消失,s.a
和s.c
被优化掉,return s.a+s.c;
不断折叠成return 5;
。 在这种情况下,编译器可以确保结构永远不会"转义",因此可以根据需要对其进行优化。