繁体   English   中英

指针指向指针-性能下降的原因

[英]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] ,并且由于amain的非易失性局部变量,因此它也知道您没有修改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

由于adouble的数组, &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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM