[英]pointers on pointers - reason for performance penalty
我回答了这个问题 ,并注意到我认为编译器的异常行为。
我首先编写了这个程序(作为我在那里回答的一部分):
class Vector {
private:
double** ptr;
public:
Vector(double** _ptr): ptr(_ptr) {}
inline double& operator[](const int iIndex) const {
return *ptr[iIndex];
}
};
extern "C" int test(const double a);
int main() {
double a[2] = { 1.0, 2.0 };
Vector va((double**) &a);
double a1 = va[0];
test(a1);
double a2 = va[0];
test(a2);
}
使用以下命令编译时会生成两个加载指令:
clang -O3 -S -emit-llvm main.cpp -o main.ll
这可以在llvm-IR中看到(并且可以在程序集中看到):
define i32 @main() #0 { entry: %a.sroa.0.0.copyload = load double*, double** bitcast ([2 x double]* @_ZZ4mainE1a to double**), align 16 %0 = load double, double* %a.sroa.0.0.copyload, align 8, !tbaa !2 %call1 = tail call i32 @test(double %0) %1 = load double, double* %a.sroa.0.0.copyload, align 8, !tbaa !2 %call3 = tail call i32 @test(double %1) ret i32 0 }
我希望只有一条加载指令,因为没有调用对内存有副作用的函数,并且我没有将此对象链接到具有副作用的对象。 实际上,在阅读程序时,我只希望有两个调用
test(1.0);
因为我的数组在内存中是恒定的,并且所有内容都可以正确内联。
可以肯定的是,我将双指针替换为一个简单的指针:
class Vector {
private:
double* ptr;
public:
Vector(double* _ptr): ptr(_ptr) {}
inline double& operator[](const int iIndex) const {
return ptr[iIndex];
}
};
extern "C" int test(const double a);
int main() {
double a[2] = { 1.0, 2.0 };
Vector va(a);
double a1 = va[0];
test(a1);
double a2 = va[0];
test(a2);
}
用同一行编译,我得到了预期的结果:
define i32 @main() #0 {
entry:
%call1 = tail call i32 @test(double 1.000000e+00)
%call3 = tail call i32 @test(double 1.000000e+00)
ret i32 0
}
看起来更好地优化了:)
因此,我的问题是:
是什么原因阻止了编译器对第一个代码样本执行相同的内联? 那是双指针吗?
在第二个代码中,编译器尝试访问:
va.ptr[0]
编译器可以推断va.ptr
与&a[0]
,并且由于a
是main
的非易失性局部变量,因此它也知道您没有修改a[0]
( test
没有“访问权限”到a
),所以它可以降低代码的一个简单的调用test
与恒定值。
但是,在您的第一个代码中,编译器知道它正在尝试访问:
*(((double**)&a)[index])
尽管((double**)&a)[index]
可能由编译器推断出来(这是与编译器相关的值),但是您将获得指向诸如0x3ff0000000000000
的地址的指针(在我的计算机上)。 上面的表达式然后尝试执行的操作是访问存储在该地址的值,但是可以通过test
甚至其他方式修改该值-编译器没有理由假设此地址的值在两次寻址之间不会发生变化。第一次访问,第二次访问。
请注意,如果您使用double (*)[2]
而不是使用double**
,则将获得与第二个代码相同的输出,并且您的代码将格式正确。
您的第一个代码基本上等效于:
extern "C" int test(const double a);
int main() {
double a[2] = { 1.0, 2.0 };
double **pp = (double**)&a;
double *p = pp[0];
double a1 = *p;
test(a1);
double a2 = *p;
test(a2);
}
使用命令行将获得相同的反汇编。
假设一个具有4个字节的double
和指针的体系结构,则在执行时会得到以下内容:
0x7fff4f40 0x3f800000 # 1.0
0x7fff4f44 0x40000000 # 2.0
由于a
是double
的数组, &a
可能会衰减为double (*)[2]
“,其值为” 0x7fff4f40
。
现在,您将&a
转换为double**
,因此您将拥有double **pp
,其值为0x7fff4f40
。 在这里,您使用pp[0]
检索double *p
0x3f800000
double *p
,因为在我的假设体系结构上指针也是4个字节,因此您将获得0x3f800000
。
太好了,因此编译器可能可以对此进行优化,基本上它可以创建如下内容:
double *p = (double*) 0x3f800000;
double a1 = *p;
test(a1);
double a2 = *p;
test(a2);
知道一百万美元的问题是:地址0x3f80000
是什么? 好吧,没人知道,甚至是编译器。 可以随时通过调用test()
甚至通过外部源来修改此地址上的值。
我不是有关double
和指针类型的大小约束的专家,但是让我们假设一个假设的体系结构,其中sizeof(double*) > 2 * sizeof(double)
,编译器甚至无法推断p
因为您将尝试访问外部a
值。
错误在以下几行中:
double a[2] = { 1.0, 2.0 };
Vector<double> va((double**) &a);
a
是两个双精度数组。 它衰减为double *
,但&a
不是 double **
。 数组和指针不是同一动物 。
实际上,您具有以下内容: (void *) a == (void *) &a
因为数组的地址是其第一个元素的地址。
如果要构建指向指针的指针,则必须显式创建一个真正的指针:
double a[2] = { 1.0, 2.0 };
double *pt = a; // or &(a[0]) ...
Vector<double> va((double**) &pt);
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.