提问人:glades 提问时间:8/27/2022 更新时间:8/28/2022 访问量:98
复制与参考基准:何时对象未在寄存器中传递?
Copy vs ref benchmark: When are objects NOT passed in registers?
问:
我听说小对象在函数调用时在 CPU 寄存器中传递。当情况不再如此时,我试图搜索最大值。我知道这是在寄存器中传递的。但极限是什么?我知道这与架构有关,但似乎即使是非常大的对象也可以通过寄存器传递。比较以下内容:string_view
(在 Quickbench 上查看)
struct big_object
{
int a_;
double b_;
char c_;
long long d_;
bool e_;
int f_;
double g_;
char h_;
long long i_;
bool j_;
int k_;
double l_;
char m_;
long long n_;
bool o_;
};
big_object obj = {
.a_ = 2,
.b_ = 2.5,
.c_ = 'A',
.d_ = 1203912045891732283,
.e_ = false,
.f_ = 10,
.g_ = 15.5,
.h_ = 'D',
.i_ = 123123123,
.j_ = true,
.k_ = 10,
.l_ = 15.5,
.m_ = 'D',
.n_ = 123123123,
.o_ = true,
};
volatile int a;
volatile double b;
volatile char c;
volatile long long d;
volatile bool e;
volatile int f;
volatile double g;
volatile char h;
volatile long long i;
volatile bool j;
volatile int k;
volatile double l;
volatile char m;
volatile long long n;
volatile bool o;
int foo(big_object obj)
{
a = obj.a_;
b = obj.b_;
c = obj.c_;
d = obj.d_;
e = obj.e_;
f = obj.f_;
g = obj.g_;
h = obj.h_;
i = obj.i_;
j = obj.j_;
k = obj.k_;
l = obj.l_;
m = obj.m_;
n = obj.n_;
o = obj.o_;
return 1;
}
int foo_ref(big_object& obj)
{
a = obj.a_;
b = obj.b_;
c = obj.c_;
d = obj.d_;
e = obj.e_;
f = obj.f_;
g = obj.g_;
h = obj.h_;
i = obj.i_;
j = obj.j_;
k = obj.k_;
l = obj.l_;
m = obj.m_;
n = obj.n_;
o = obj.o_;
return 1;
}
static void Foo(benchmark::State& state) {
// Code inside this loop is measured repeatedly
for (auto _ : state) {
foo(obj);
}
}
// Register the function as a benchmark
BENCHMARK(Foo);
static void FooRef(benchmark::State& state) {
// Code before the loop is not measured
for (auto _ : state) {
foo_ref(obj);
}
}
BENCHMARK(FooRef);
这将编译为完全相同的代码,从而提供相同的性能。我不是汇编专家,但我想我可以看到很多寄存器用于将对象传递给函数。我知道物理 CPU 寄存器比逻辑寄存器还要多,但这是否意味着在任何实际情况下,通过引用传递实际上是多余的?
答:
当函数调用被优化时,当函数被内联时,当你使用指针而不是对象时,编译器决定它不值得复制,仅举几例。除了 C++ 标准之外,编译器程序员没有必须遵循的明确规则集。
但这是否意味着在任何实际情况下,通过引用实际上是多余的?
不,它不是,引用是一种语义结构,而不是优化“技巧”,所以这样对待它们。
引用只是变量的别名。其他一切都取决于编译器。可变引用的一个有用的正确之处是,一旦函数返回,变异对象实际上也会在调用方端发生变化。
当然,您也可以使用指针来实现相同的目的,但更有用的引用属性是保证您将访问的对象是有效的,而指针是可为空的。
说到这一点,现在甚至指针的处理方式或多或少都是一样的,只要看看 GCC,以类似的方式,编译器可以优化出来,简单地直接复制。我不记得这个优化的名称,但我知道一个事实,GCC 至少曾经拥有它。如果你有一个像这样的函数,它在大多数情况下都会被内联,所以你如何编写它并不重要。这只是选择最清晰版本的问题。-findirect-inlining
float*
float
float add(float* a, float* b)
与其担心你不理解的东西,甚至不需要担心,不如把重点放在代码的语义上。说实话,你使用像C++这样的高级语言,而不是自己手动优化所有的ISA,正是因为你想让别人为你做艰苦的工作,所以当这是他们唯一的工作时,你为什么不信任他们呢?
例如,仅仅为了复制对象而作为参数是没有意义的。如果你想在函数中有一个对象的副本,那就这样写。这不仅对程序员来说更干净,更明显地表明您将要复制,而且甚至对编译器来说也更清晰,这也是由程序员制作的。您从某些优化中受益的事实是这里唯一多余的东西。你只是让任何阅读声明的人都清楚,这个对象将被复制,这才是最重要的。不管有多慢,你都需要复制,所以你复制。const&
编写可读和可理解的代码,当您有一个工作程序时,您可能会担心性能。如果你能想到一些“巧妙”的东西,事实是编译器程序员早就想到了。
顺便说一句,你的基准测试不会对任何东西进行基准测试。您实际上是在比较相同的代码。
所以就像我之前已经写过的,你想复制结构吗?或者只是引用其中的某些内容?这是唯一重要的事情。
下一个:向量如何按值传递?
评论
foo
并已内联在程序集的链接中。根本没有发生函数调用。foo_ref
volatile