Tôi muốn cố gắng cung cấp một câu trả lời hơi toàn diện hơn sau khi điều này được thảo luận với ủy ban tiêu chuẩn C ++. Ngoài việc là thành viên của ủy ban C ++, tôi còn là nhà phát triển về trình biên dịch LLVM và Clang.
Về cơ bản, không có cách nào sử dụng một rào cản hoặc một số hoạt động trong trình tự để đạt được những biến đổi này. Vấn đề cơ bản là ngữ nghĩa hoạt động của một cái gì đó giống như một phép cộng số nguyên hoàn toàn được biết đến khi triển khai. Nó có thể mô phỏng chúng, nó biết chúng không thể được quan sát bởi các chương trình chính xác và luôn tự do di chuyển chúng xung quanh.
Chúng tôi có thể cố gắng ngăn chặn điều này, nhưng nó sẽ có kết quả cực kỳ tiêu cực và cuối cùng sẽ thất bại.
Đầu tiên, cách duy nhất để ngăn chặn điều này trong trình biên dịch là cho nó biết rằng tất cả các hoạt động cơ bản này đều có thể quan sát được. Vấn đề là điều này sau đó sẽ loại trừ phần lớn các tối ưu hóa trình biên dịch. Bên trong trình biên dịch, về cơ bản chúng ta không có cơ chế tốt nào để mô hình hóa rằng thời gian có thể quan sát được chứ không có gì khác. Chúng tôi thậm chí không có một mô hình tốt về những gì hoạt động cần thời gian . Ví dụ: việc chuyển đổi số nguyên không dấu 32 bit thành số nguyên không dấu 64 bit có mất thời gian không? Nó mất thời gian bằng 0 trên x86-64, nhưng trên các kiến trúc khác thì mất thời gian khác 0. Không có câu trả lời chung chung chính xác ở đây.
Nhưng ngay cả khi chúng ta thành công thông qua một số anh hùng trong việc ngăn trình biên dịch sắp xếp lại các hoạt động này, không có gì đảm bảo điều này là đủ. Hãy xem xét một cách hợp lệ và phù hợp để thực thi chương trình C ++ của bạn trên máy x86: DynamoRIO. Đây là một hệ thống đánh giá động mã máy của chương trình. Một điều nó có thể làm là tối ưu hóa trực tuyến và thậm chí nó có khả năng thực thi toàn bộ phạm vi của các hướng dẫn số học cơ bản bên ngoài thời gian. Và hành vi này không phải là duy nhất đối với các trình đánh giá động, CPU x86 thực tế cũng sẽ suy đoán (một số lượng nhỏ hơn nhiều) các hướng dẫn và sắp xếp lại chúng một cách động.
Nhận thức cơ bản là thực tế rằng số học không thể quan sát được (ngay cả ở cấp độ thời gian) là một cái gì đó xuyên suốt các lớp của máy tính. Nó đúng với trình biên dịch, thời gian chạy và thường là cả phần cứng. Việc buộc nó phải có thể quan sát được sẽ hạn chế đáng kể trình biên dịch, nhưng nó cũng sẽ hạn chế đáng kể phần cứng.
Nhưng tất cả những điều này không nên khiến bạn mất hy vọng. Khi bạn muốn tính thời gian thực hiện các phép toán cơ bản, chúng tôi đã nghiên cứu kỹ các kỹ thuật hoạt động đáng tin cậy. Thông thường, chúng được sử dụng khi thực hiện đo điểm chuẩn vi mô . Tôi đã nói chuyện về điều này tại CppCon2015: https://youtu.be/nXaxk27zwlk
Các kỹ thuật được hiển thị ở đó cũng được cung cấp bởi các thư viện vi điểm chuẩn khác nhau như: https://github.com/google/benchmark#preventing-optimization của Google
Chìa khóa của các kỹ thuật này là tập trung vào dữ liệu. Bạn làm cho đầu vào tính toán không rõ ràng đối với trình tối ưu hóa và kết quả của tính toán không rõ ràng đối với trình tối ưu hóa. Khi bạn đã hoàn thành điều đó, bạn có thể tính thời gian một cách đáng tin cậy. Hãy xem xét phiên bản thực tế của ví dụ trong câu hỏi ban đầu, nhưng với định nghĩa foo
hiển thị đầy đủ cho việc triển khai. Tôi cũng đã trích xuất phiên bản (không di động) của DoNotOptimize
thư viện Google Benchmark mà bạn có thể tìm thấy tại đây: https://github.com/google/benchmark/blob/master/include/benchmark/benchmark_api.h#L208
#include <chrono>
template <class T>
__attribute__((always_inline)) inline void DoNotOptimize(const T &value) {
asm volatile("" : "+m"(const_cast<T &>(value)));
}
// The compiler has full knowledge of the implementation.
static int foo(int x) { return x * 2; }
auto time_foo() {
using Clock = std::chrono::high_resolution_clock;
auto input = 42;
auto t1 = Clock::now(); // Statement 1
DoNotOptimize(input);
auto output = foo(input); // Statement 2
DoNotOptimize(output);
auto t2 = Clock::now(); // Statement 3
return t2 - t1;
}
Ở đây chúng tôi đảm bảo rằng dữ liệu đầu vào và dữ liệu đầu ra được đánh dấu là không thể tối ưu hóa xung quanh quá trình tính toán foo
và chỉ xung quanh các điểm đánh dấu đó mới được tính toán thời gian. Bởi vì bạn đang sử dụng dữ liệu để xác định tính toán, nó được đảm bảo nằm giữa hai thời gian và bản thân tính toán vẫn được phép tối ưu hóa. Kết quả x86-64 lắp ráp được tạo bởi bản dựng gần đây của Clang / LLVM là:
% ./bin/clang++ -std=c++14 -c -S -o - so.cpp -O3
.text
.file "so.cpp"
.globl _Z8time_foov
.p2align 4, 0x90
.type _Z8time_foov,@function
_Z8time_foov: # @_Z8time_foov
.cfi_startproc
# BB#0: # %entry
pushq %rbx
.Ltmp0:
.cfi_def_cfa_offset 16
subq $16, %rsp
.Ltmp1:
.cfi_def_cfa_offset 32
.Ltmp2:
.cfi_offset %rbx, -16
movl $42, 8(%rsp)
callq _ZNSt6chrono3_V212system_clock3nowEv
movq %rax, %rbx
#APP
#NO_APP
movl 8(%rsp), %eax
addl %eax, %eax # This is "foo"!
movl %eax, 12(%rsp)
#APP
#NO_APP
callq _ZNSt6chrono3_V212system_clock3nowEv
subq %rbx, %rax
addq $16, %rsp
popq %rbx
retq
.Lfunc_end0:
.size _Z8time_foov, .Lfunc_end0-_Z8time_foov
.cfi_endproc
.ident "clang version 3.9.0 (trunk 273389) (llvm/trunk 273380)"
.section ".note.GNU-stack","",@progbits
Ở đây, bạn có thể thấy trình biên dịch tối ưu hóa lệnh gọi foo(input)
xuống một lệnh duy nhất addl %eax, %eax
, nhưng không di chuyển nó ra ngoài thời gian hoặc loại bỏ hoàn toàn mặc dù đầu vào không đổi.
Hy vọng điều này sẽ hữu ích và ủy ban tiêu chuẩn C ++ đang xem xét khả năng tiêu chuẩn hóa các API tương tự như DoNotOptimize
ở đây.