关于指针的指针 - 性能损失的原因

时间:2016-05-19 12:40:39

标签: c++ pointers optimization clang inline

我回答this question,并注意到我认为是编译器的奇怪行为。

我第一次写这个程序(作为我答案的一部分):

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
}

看起来更优化的方式:)

因此我的问题是:

什么原因阻止编译器对第一个代码示例执行相同的内联?这是双指针吗?

2 个答案:

答案 0 :(得分:2)

在第二个代码中,编译器尝试访问:

va.ptr[0]

编译器可以推断va.ptr&a[0]相同,并且由于amain的非易失性局部变量,它也知道你做了不要修改a[0]test没有a的“访问权限”,因此它可以将您的代码简化为对test的简单调用。

然而,在您的第一个代码中,编译器知道它正在尝试访问:

*(((double**)&a)[index])

虽然((double**)&a)[index]可能由编译器推断(这是依赖于编译器的值),但您将获得指向0x3ff0000000000000(在我的计算机上)的地址的指针。上面的表达式然后尝试做的是访问存储在该地址的值,但是这个值可以被test修改,或者甚至被其他东西修改 - 编译器没有理由认为这个地址的值是不要在第一次访问和第二次访问之间进行更改。

请注意,如果不是使用double**,而是使用double (*)[2],那么您将获得与第二个代码相同的输出,并且您的代码格式正确。

您的第一个代码基本上等同于:

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。从这里开始,您使用double *p检索pp[0],因为我的假设架构上的指针也是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以外的值。

答案 1 :(得分:2)

错误在以下几行:

double a[2] = { 1.0, 2.0 };
Vector<double> va((double**) &a);

a是两个双打的数组。它衰减double *,但&a a double **数组和指针不是同一种动物

实际上你有以下内容:(void *) a == (void *) &a因为数组的地址是第一个元素的地址。

如果要构建指针指针,则必须明确地创建一个真正的指针:

double a[2] = { 1.0, 2.0 };
double *pt = a; // or &(a[0]) ...
Vector<double> va((double**) &pt);