2016-05-19 14 views
6

Tôi đã trả lời this question và nhận thấy những gì tôi coi là hành vi lạ của trình biên dịch.con trỏ trên con trỏ - lý do cho hình phạt hiệu suất

đầu tiên tôi đã viết chương trình này (như là một phần của câu trả lời của tôi ở đó):

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); 
} 

mà tạo ra hai hướng dẫn tải khi biên soạn với:

clang -O3 -S -emit-llvm main.cpp -o main.ll 

Điều này có thể được nhìn thấy trong llvm-IR (và có thể được nhìn thấy trong hội đồng):

 
    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 
    } 

Tôi chỉ mong đợi một hướng dẫn tải, vì không chức năng với tác dụng phụ trên bộ nhớ đã được gọi, và tôi đã không liên kết đối tượng này với một cái gì đó với các tác dụng phụ. Trên thực tế, khi đọc chương trình, tôi chỉ mong đợi hai cuộc gọi đến

test(1.0); 

vì mảng của tôi không đổi trong bộ nhớ và mọi thứ có thể được in đúng cách.

Chỉ để chắc chắn, tôi đã thay thế con trỏ đôi bởi một con trỏ đơn giản:

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); 
} 

Biên soạn với cùng một dòng, tôi nhận được kết quả mong đợi:

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 
} 

nào trông giống như cách tốt hơn tối ưu :)

câu hỏi của tôi do đó là:

Lý do nào ngăn trình biên dịch thực hiện cùng nội tuyến trên mẫu mã đầu tiên? Đó có phải là con trỏ kép không?

+4

Một con trỏ tới một mảng của 'double' là * không * giống như một con trỏ đến một con trỏ đến một gấp đôi. Khi bạn thực hiện '& a' bạn nhận được một cái gì đó của kiểu' double (*) [2] '. Nếu bạn cần phải tạo kiểu C trong C++, thì đó là dấu hiệu của một thứ gì đó không hoàn toàn đúng. –

+0

Cảm ơn bạn đã tăng điểm quan trọng. Điều đó giải thích bất cứ điều gì về phần còn lại của chương trình? –

+1

Trong mã đầu tiên của bạn, bạn đang cố gắng truy cập bộ nhớ bên ngoài những gì trình biên dịch có thể thấy, vì vậy không có lý do nào trình biên dịch giả định rằng 'test' không sửa đổi vị trí bộ nhớ này. – Holt

Trả lời

2

Lỗi là ở những dòng này:

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

a là một mảng của hai đôi. Nó phân rã thành double *, nhưng &akhông a double **. Mảng và con trỏ không giống động vật.

Thực tế bạn có: (void *) a == (void *) &a vì địa chỉ của mảng là địa chỉ của phần tử đầu tiên.

Nếu bạn muốn xây dựng một con trỏ đến con trỏ bạn phải tạo explicitely một con trỏ đúng:

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

Điều đó đúng, biên dịch với gcc ví dụ về Regis, chúng ta nhận được 'movabsq <1.0d>,% rbx; movsd (% rbx),% xmm0', thực sự có nghĩa là, dereference con trỏ của địa chỉ '0x3FF0000000000000 = 1.0d'. –

+0

Với sửa đổi của bạn, tôi nhận được gấp đôi cùng một hội đồng. (kiểm tra cuộc gọi (1.0)). Thiệt hại ... –

2

Trong mã thứ hai của bạn, trình biên dịch cố gắng truy cập:

va.ptr[0] 

Trình biên dịch có thể suy ra rằng va.ptr cũng giống như &a[0], và kể từ a là một biến địa phương không dễ bay hơi của main, nó cũng biết rằng bạn không sửa đổi a[0] (test không có quyền truy cập vào a), vì vậy, nó có thể làm giảm mã của bạn thành một cuộc gọi đơn giản tới test với giá trị không đổi.

Trong mã đầu tiên của bạn tuy nhiên, trình biên dịch biết rằng nó đang cố gắng truy cập:

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

Trong khi ((double**)&a)[index] có thể suy ra bởi trình biên dịch (đây là một giá trị biên dịch phụ thuộc), bạn sẽ nhận được một con trỏ đến một địa chỉ như 0x3ff0000000000000 (trên máy tính của tôi). Những gì biểu thức trên sau đó cố gắng làm là truy cập giá trị được lưu trữ tại địa chỉ này, nhưng giá trị này có thể được sửa đổi bởi test, hoặc thậm chí bởi cái gì khác - Không có lý do trình biên dịch có thể giả định giá trị tại địa chỉ này không thay đổi giữa truy cập đầu tiên và thứ hai.

Lưu ý rằng nếu thay vì sử dụng double**, bạn đã sử dụng double (*)[2], bạn sẽ nhận được kết quả tương tự như với mã thứ hai và mã của bạn sẽ được tạo đúng.


mã đầu tiên của bạn là về cơ bản tương đương với:

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); 
} 

Bạn sẽ nhận được tháo gỡ cùng sử dụng dòng lệnh của bạn.

Giả sử một kiến ​​trúc với 4 byte double và con trỏ, bạn sẽ có được một cái gì đó như thế này tại thực hiện:

0x7fff4f40 0x3f800000 # 1.0 
0x7fff4f44 0x40000000 # 2.0 

Kể từ a là một mảng của double, &a có thể phân hủy thành một double (*)[2] "với một giá trị của" 0x7fff4f40 .

Bây giờ, bạn đang chuyển đổi &a thành double**, vì vậy bạn sẽ có một double **pp với giá trị là 0x7fff4f40. Từ đây, bạn lấy một double *p bằng cách sử dụng pp[0], vì con trỏ cũng là 4 byte trên kiến ​​trúc giả định của tôi, bạn sẽ nhận được 0x3f800000.

vĩ đại, vì vậy trình biên dịch có thể tối ưu hóa lên đến nay, về cơ bản nó có thể tạo ra một cái gì đó như thế này:

double *p = (double*) 0x3f800000; 

double a1 = *p; 
test(a1); 

double a2 = *p; 
test(a2); 

Biết USD câu hỏi một triệu là: Điều gì là tại địa chỉ 0x3f80000? Vâng, không ai biết, ngay cả trình biên dịch. Giá trị tại địa chỉ này có thể được sửa đổi bất cứ lúc nào, bằng một cuộc gọi đến test() hoặc thậm chí bởi một nguồn bên ngoài.

Tôi không phải là một chuyên gia về những hạn chế kích thước trên double và con trỏ loại, nhưng chúng ta hãy giả định một kiến ​​trúc giả thuyết nơi sizeof(double*) > 2 * sizeof(double), trình biên dịch sẽ không thậm chí có thể suy ra p bởi vì bạn sẽ phải cố gắng để giá trị truy cập bên ngoài a.

+0

Cảm ơn câu trả lời. Tuy nhiên, trong trường hợp đầu tiên, trình biên dịch nên biết rằng một đã được cấp phát trên ngăn xếp chính. Do đó, địa chỉ của một địa chỉ của một số biến ngăn xếp cục bộ. Nội dung của địa chỉ này có thể được người khác sửa đổi như thế nào? Nếu kiểm tra làm hỏng ngăn người gọi? –

+0

@RegisPortalez Xem bản chỉnh sửa. – Holt

+0

Làm cho tinh thần. Cảm ơn. Tôi sẽ đợi bất kỳ câu trả lời nào khác trước khi chấp nhận nó. –

Các vấn đề liên quan