2016-10-19 20 views
6

Gần đây tôi đã đi qua cpp2015 nói chuyện rực rỡ này CppCon 2015: Chandler Carruth "Tuning C++: Benchmarks, and CPUs, and Compilers! Oh My!"Ngăn chặn tối ưu hóa trình biên dịch trong khi điểm chuẩn

Một trong những kỹ thuật đề cập để ngăn chặn các trình biên dịch từ tối ưu hóa mã đang sử dụng các chức năng dưới đây.

static void escape(void *p) { 
    asm volatile("" : : "g"(p) : "memory"); 
} 

static void clobber() { 
    asm volatile("" : : : "memory"); 
} 

void benchmark() 
{ 
    vector<int> v; 
    v.reserve(1); 
    escape(v.data()); 
    v.push_back(10); 
    clobber() 
} 

Tôi đang cố gắng hiểu điều này. Câu hỏi như sau.

1) Lợi thế của việc trốn thoát trên clobber là gì?

2) Từ ví dụ trên có vẻ như clobber() ngăn chặn câu lệnh trước (push_back) được tối ưu hóa. Nếu đó là lý do tại sao đoạn mã dưới đây không chính xác?

void benchmark() 
{ 
    vector<int> v; 
    v.reserve(1); 
    v.push_back(10); 
    clobber() 
} 

Nếu đây là không đủ gây nhầm lẫn, sự điên rồ (FB của luồng lib) có một thậm chí stranger implementation

đoạn liên quan:

template <class T> 
void doNotOptimizeAway(T&& datum) { 
    asm volatile("" : "+r" (datum)); 
} 

hiểu biết của tôi là thật kỹ xem mã trên thông báo cho trình biên dịch mà khối lắp ráp sẽ ghi vào datum. Nhưng nếu trình biên dịch tìm thấy không có người tiêu dùng của datum này nó vẫn có thể tối ưu hóa ra các thực thể sản xuất datum phải không?

Tôi cho rằng đây không phải là kiến ​​thức phổ biến và mọi trợ giúp đều được đánh giá cao!

+2

Thực ra, "+ r" có nghĩa là đoạn mã sẽ đọc và ghi dữ liệu. Vì trình biên dịch (gcc) không thể biết làm thế nào datum có thể được sử dụng/sửa đổi bởi asm, nó không thể tối ưu hóa nó đi trong bất kỳ trường hợp nào. –

+1

Chức năng đó được tích hợp sẵn vào google benchmark ngay bây giờ. điểm chuẩn :: DoNotOptimize như được thấy ở đây: github.com/google/benchmark – xaxxon

Trả lời

3

1) Lợi thế của việc trốn thoát trên clobber là gì?

escape() không có lợi thế hơn clobber(). escape()bổ sungclobber() trong cách quan trọng sau đây:

Hiệu quả của clobber() được giới hạn bộ nhớ đó là khả năng truy cập thông qua một con trỏ gốc toàn cầu tưởng tượng. Nói cách khác, mô hình trình biên dịch của bộ nhớ được cấp phát là một biểu đồ kết nối của các khối liên quan đến nhau thông qua con trỏ, và con trỏ gốc toàn cầu tưởng tượng được dùng như một điểm vào cho đồ thị đó. (Rò rỉ bộ nhớ không được tính trong mô hình này, tức là trình biên dịch bỏ qua khả năng một khi các khối truy cập có thể trở nên không thể truy cập được vì giá trị con trỏ bị mất). Khối mới được phân bổ không phải là một phần của biểu đồ như vậy và không bị ảnh hưởng bởi bất kỳ tác dụng phụ nào của clobber(). escape() đảm bảo rằng địa chỉ được truyền trong thuộc bộ khối bộ nhớ có thể truy cập toàn cầu. Khi được áp dụng cho một khối bộ nhớ mới được phân bổ, escape() có tác dụng thêm nó vào biểu đồ đã nói.

2) Từ ví dụ trên có vẻ như clobber() ngăn chặn câu lệnh trước trước đó (push_back) được tối ưu hóa. Nếu đó là trường hợp tại sao đoạn mã bên dưới không chính xác?

void benchmark() 
{ 
    vector<int> v; 
    v.reserve(1); 
    v.push_back(10); 
    clobber(); 
} 

Việc phân bổ ẩn bên trong v.reserve(1) là không hiển thị cho clobber() cho đến khi nó được đăng ký qua escape().

4

tl; dr doNotOptimizeAway tạo "sử dụng" nhân tạo.

Một chút thuật ngữ ở đây: "def" ("định nghĩa") là một câu lệnh gán giá trị cho biến; "use" là một câu lệnh, sử dụng giá trị của một biến để thực hiện một số thao tác.

Nếu từ điểm ngay sau khi thoát, tất cả các đường dẫn đến thoát chương trình sẽ không gặp phải việc sử dụng biến số, lỗi đó được gọi là dead và vé loại bỏ mã chết (DCE) sẽ xóa nó. Mà lần lượt có thể gây ra các defs khác để trở thành chết (nếu def đó là một sử dụng bởi đức hạnh của có operands biến), vv

Hãy tưởng tượng chương trình sau khi thay thế Scalar tổng hợp (SRA), biến địa phương std::vector trong hai biến số lenptr. Tại một số điểm chương trình gán một giá trị cho ptr; tuyên bố đó là một def.

Hiện tại, chương trình gốc không làm bất kỳ điều gì với vectơ; nói cách khác không có bất kỳ việc sử dụng nào là len hoặc ptr. Do đó, tất cả các defs của họ đã chết và DCE có thể loại bỏ chúng, có hiệu quả loại bỏ tất cả các mã và làm cho các điểm chuẩn vô giá trị.

Thêm doNotOptimizeAway(ptr) tạo sử dụng nhân tạo, ngăn DCE xóa các lỗi. (Là một lưu ý phụ, tôi thấy không có điểm trong "+", "g" nên đã đủ).

Một dòng tương tự của lý luận có thể được theo sau với tải bộ nhớ và cửa hàng: một cửa hàng (def) là chết iff không có đường dẫn đến cuối chương trình, có chứa tải (một sử dụng) từ vị trí cửa hàng đó. Khi theo dõi các vị trí bộ nhớ tùy ý khó hơn việc theo dõi các biến đăng ký giả riêng lẻ, lý do trình biên dịch bảo thủ - một cửa hàng bị chết nếu không có đường dẫn đến cuối chương trình, có thể có thể là gặp phải việc sử dụng cửa hàng đó.

Một trường hợp như vậy, là một cửa hàng đến một vùng bộ nhớ, được đảm bảo không được đặt bí danh - sau khi bộ nhớ được deallocated, có thể không có thể được sử dụng của cửa hàng đó, mà không kích hoạt hành vi không xác định. IOW, không có sử dụng như vậy.

Do đó trình biên dịch có thể loại bỏ v.push_back(42). Nhưng có escape - nó làm cho v.data() được coi là bí danh tùy ý, như @Leon được mô tả ở trên.

Mục đích của clobber() trong ví dụ này là tạo ra một sử dụng nhân tạo của tất cả bộ nhớ bí danh. Chúng tôi có một cửa hàng (từ push_back(42)), cửa hàng là một địa điểm được đặt biệt hiệu toàn cầu (do escape(v.data())), do đó clobber() có khả năng chứa việc sử dụng cửa hàng đó (IOW, hiệu ứng bên cửa hàng sẽ được quan sát), do đó trình biên dịch không được phép xóa cửa hàng.

Một vài ví dụ đơn giản:

Ví dụ I:

void f() { 
    int v[1]; 
    v[0] = 42; 
} 

này không tạo ra bất kỳ mã.

Ví dụ II:

extern void g(); 

void f() { 
    int v[1]; 
    v[0] = 42; 
    g(); 
} 

này tạo ra chỉ là một cuộc gọi đến g(), không có bộ nhớ lưu trữ. Hàm g không thể truy cập vv không được đặt bí danh.

Ví dụ III:

void clobber() { 
    __asm__ __volatile__ ("" : : : "memory"); 
} 

void f() { 
    int v[1]; 
    v[0] = 42; 
    clobber(); 
} 

Giống như trong ví dụ trước, không có cửa hàng tạo vì v không aliased và cuộc gọi đến clobber được inlined không có gì.

Ví dụ IV:

template<typename T> 
void use(T &&t) { 
    __asm__ __volatile__ ("" :: "g" (t)); 
} 

void f() { 
    int v[1]; 
    use(v); 
    v[0] = 42; 
} 

Lần này v thoát (ví dụ: có thể có khả năng truy cập từ khung kích hoạt khác). Tuy nhiên, cửa hàng vẫn bị xóa, vì sau khi nó không có khả năng sử dụng bộ nhớ đó (không có UB).

Ví dụ V:

template<typename T> 
void use(T &&t) { 
    __asm__ __volatile__ ("" :: "g" (t)); 
} 

extern void g(); 

void f() { 
    int v[1]; 
    use(v); 
    v[0] = 42; 
    g(); // same with clobber() 
} 

Và cuối cùng chúng tôi nhận được cửa hàng, bởi vì v thoát và trình biên dịch phải thận trọng cho rằng cuộc gọi đến g có thể truy cập các giá trị được lưu trữ.

(đối với thử nghiệm https://godbolt.org/g/rFviMI)

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