Thực thi thứ tự câu lệnh trong C ++


111

Giả sử tôi có một số câu lệnh mà tôi muốn thực hiện theo một thứ tự cố định. Tôi muốn sử dụng g ++ với mức tối ưu hóa 2, vì vậy một số câu lệnh có thể được sắp xếp lại. Những công cụ nào người ta có để thực thi một thứ tự nhất định của các câu lệnh?

Hãy xem xét ví dụ sau.

using Clock = std::chrono::high_resolution_clock;

auto t1 = Clock::now(); // Statement 1
foo();                  // Statement 2
auto t2 = Clock::now(); // Statement 3

auto elapsedTime = t2 - t1;

Trong ví dụ này, điều quan trọng là các câu lệnh 1-3 được thực hiện theo thứ tự nhất định. Tuy nhiên, trình biên dịch không thể nghĩ rằng câu lệnh 2 là độc lập với 1 và 3 và thực thi mã như sau?

using Clock=std::chrono::high_resolution_clock;

foo();                  // Statement 2
auto t1 = Clock::now(); // Statement 1
auto t2 = Clock::now(); // Statement 3

auto elapsedTime = t2 - t1;

34
Nếu trình biên dịch cho rằng chúng độc lập khi chúng không độc lập, thì trình biên dịch đã bị hỏng và bạn nên sử dụng trình biên dịch tốt hơn.
David Schwartz


1
có thể __sync_synchronize()được bất kỳ giúp đỡ?
vsz

3
@HowardHinnant: Sức mạnh ngữ nghĩa của tiêu chuẩn C sẽ được cải thiện đáng kể nếu một chỉ thị như vậy được xác định và nếu các quy tắc răng cưa được điều chỉnh để miễn các lần đọc được thực hiện sau một rào cản dữ liệu được viết trước nó.
supercat

4
@DavidSchwartz Trong trường hợp này là đo thời gian foocần thiết để chạy, mà trình biên dịch được phép bỏ qua khi sắp xếp lại, giống như nó được phép bỏ qua quan sát từ một luồng khác.
CodesInChaos

Câu trả lời:


100

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 foohiể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 DoNotOptimizethư 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 foovà 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.


1
Cảm ơn bạn vì câu trả lời. Tôi đã đánh dấu nó là câu trả lời mới hay nhất. Tôi có thể đã làm điều này sớm hơn, nhưng tôi đã không đọc trang stackoverflow này trong nhiều tháng. Tôi rất quan tâm đến việc sử dụng trình biên dịch Clang để tạo các chương trình C ++. Trong số những thứ khác, tôi thích rằng người ta có thể sử dụng các ký tự Unicode trong các tên biến trong Clang. Tôi nghĩ rằng tôi sẽ hỏi nhiều câu hỏi hơn về Clang trên Stackoverflow.
S2108887 10/12/16

5
Trong khi tôi hiểu cách điều này ngăn foo được tối ưu hóa hoàn toàn, bạn có thể giải thích một chút tại sao điều này ngăn các lệnh gọi Clock::now()được sắp xếp lại liên quan đến foo () không? Liệu trình tối ưu có phải giả định điều đó DoNotOptimizeClock::now()có quyền truy cập và có thể sửa đổi một số trạng thái toàn cục chung mà đến lượt nó sẽ ràng buộc chúng với đầu vào và đầu ra không? Hay bạn đang dựa vào một số hạn chế hiện tại của việc triển khai trình tối ưu hóa?
MikeMB

2
DoNotOptimizetrong ví dụ này là một sự kiện tổng hợp "có thể quan sát được". Nó giống như thể nó in đầu ra có thể nhìn thấy một cách tùy ý cho một số đầu cuối với biểu diễn của đầu vào. Vì việc đọc đồng hồ cũng có thể quan sát được (bạn đang quan sát thời gian trôi qua) nên chúng không thể được sắp xếp lại mà không thay đổi hành vi quan sát của chương trình.
Chandler Carruth

1
Tôi vẫn chưa hoàn toàn rõ ràng với khái niệm "có thể quan sát", nếu foohàm đang thực hiện một số hoạt động như đọc từ một ổ cắm có thể bị chặn trong một thời gian, thì điều này có tính là một hoạt động có thể quan sát được không? Và vì readkhông phải là một hoạt động "hoàn toàn được biết đến" (phải không?), Mã sẽ giữ theo thứ tự?
ravenisadesk

"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." Nhưng đối với tôi, có vẻ như vấn đề không phải là ngữ nghĩa của phép cộng số nguyên, mà là ngữ nghĩa của việc gọi hàm foo (). Trừ khi foo () nằm trong cùng một đơn vị biên dịch, thì làm sao nó biết rằng foo () và clock () không tương tác?
Dave

59

Tóm lược:

Dường như không có cách nào đảm bảo để ngăn việc sắp xếp lại thứ tự, nhưng miễn là tối ưu hóa thời gian liên kết / toàn chương trình không được bật, việc định vị hàm được gọi trong một đơn vị biên dịch riêng biệt có vẻ là một lựa chọn khá tốt . (Ít nhất là với GCC, mặc dù logic sẽ gợi ý rằng điều này cũng có thể xảy ra với các trình biên dịch khác.) Điều này đi kèm với chi phí của lệnh gọi hàm - mã nội tuyến theo định nghĩa trong cùng một đơn vị biên dịch và mở để sắp xếp lại.

Câu trả lời ban đầu:

GCC sắp xếp lại các cuộc gọi dưới tối ưu hóa -O2:

#include <chrono>
static int foo(int x)    // 'static' or not here doesn't affect ordering.
{
    return x*2;
}
int fred(int x)
{
    auto t1 = std::chrono::high_resolution_clock::now();
    int y = foo(x);
    auto t2 = std::chrono::high_resolution_clock::now();
    return y;
}

GCC 5.3.0:

g++ -S --std=c++11 -O0 fred.cpp :

_ZL3fooi:
        pushq   %rbp
        movq    %rsp, %rbp
        movl    %ecx, 16(%rbp)
        movl    16(%rbp), %eax
        addl    %eax, %eax
        popq    %rbp
        ret
_Z4fredi:
        pushq   %rbp
        movq    %rsp, %rbp
        subq    $64, %rsp
        movl    %ecx, 16(%rbp)
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movq    %rax, -16(%rbp)
        movl    16(%rbp), %ecx
        call    _ZL3fooi
        movl    %eax, -4(%rbp)
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movq    %rax, -32(%rbp)
        movl    -4(%rbp), %eax
        addq    $64, %rsp
        popq    %rbp
        ret

Nhưng:

g++ -S --std=c++11 -O2 fred.cpp :

_Z4fredi:
        pushq   %rbx
        subq    $32, %rsp
        movl    %ecx, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        call    _ZNSt6chrono3_V212system_clock3nowEv
        leal    (%rbx,%rbx), %eax
        addq    $32, %rsp
        popq    %rbx
        ret

Bây giờ, với foo () như một hàm ngoại vi:

#include <chrono>
int foo(int x);
int fred(int x)
{
    auto t1 = std::chrono::high_resolution_clock::now();
    int y = foo(x);
    auto t2 = std::chrono::high_resolution_clock::now();
    return y;
}

g++ -S --std=c++11 -O2 fred.cpp :

_Z4fredi:
        pushq   %rbx
        subq    $32, %rsp
        movl    %ecx, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movl    %ebx, %ecx
        call    _Z3fooi
        movl    %eax, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movl    %ebx, %eax
        addq    $32, %rsp
        popq    %rbx
        ret

NHƯNG, nếu điều này được liên kết với -flto (tối ưu hóa thời gian liên kết):

0000000100401710 <main>:
   100401710:   53                      push   %rbx
   100401711:   48 83 ec 20             sub    $0x20,%rsp
   100401715:   89 cb                   mov    %ecx,%ebx
   100401717:   e8 e4 ff ff ff          callq  100401700 <__main>
   10040171c:   e8 bf f9 ff ff          callq  1004010e0 <_ZNSt6chrono3_V212system_clock3nowEv>
   100401721:   e8 ba f9 ff ff          callq  1004010e0 <_ZNSt6chrono3_V212system_clock3nowEv>
   100401726:   8d 04 1b                lea    (%rbx,%rbx,1),%eax
   100401729:   48 83 c4 20             add    $0x20,%rsp
   10040172d:   5b                      pop    %rbx
   10040172e:   c3                      retq

3
MSVC và ICC cũng vậy. Clang là chiếc duy nhất có vẻ bảo tồn được trình tự ban đầu.
Cody Grey

3
bạn không sử dụng t1 và t2 bất cứ nơi nào để nó có thể nghĩ rằng kết quả có thể được loại bỏ và sắp xếp lại mã
phuclv

3
@Niall - Tôi không thể đưa ra bất cứ điều gì cụ thể hơn, nhưng tôi nghĩ nhận xét của tôi ám chỉ lý do cơ bản: Trình biên dịch biết rằng foo () không thể ảnh hưởng đến now (), và ngược lại, và việc sắp xếp lại cũng vậy. Các thí nghiệm khác nhau liên quan đến các chức năng và dữ liệu phạm vi bên ngoài dường như xác nhận điều này. Điều này bao gồm việc có foo tĩnh () phụ thuộc vào biến phạm vi tệp N - nếu N được khai báo là tĩnh, việc sắp xếp lại xảy ra, trong khi nếu nó được khai báo là không tĩnh (nghĩa là nó hiển thị với các đơn vị biên dịch khác và do đó có khả năng chịu tác dụng phụ của các chức năng bên ngoài như now ()) sắp xếp lại không xảy ra.
Jeremy

3
@ Lưu Vĩnh Phúc: Ngoại trừ việc bản thân các cuộc gọi không được giải thích. Một lần nữa, tôi nghi ngờ điều này là bởi vì trình biên dịch không biết tác dụng phụ của họ có thể - nhưng nó không biết rằng những tác dụng phụ không thể ảnh hưởng đến hành vi của foo ().
Jeremy

3
Và lưu ý cuối cùng: việc chỉ định -flto (tối ưu hóa thời gian liên kết) gây ra việc sắp xếp lại ngay cả trong các trường hợp không được sắp xếp lại.
Jeremy

20

Việc sắp xếp lại thứ tự có thể được thực hiện bởi trình biên dịch hoặc bởi bộ xử lý.

Hầu hết các trình biên dịch cung cấp một phương pháp dành riêng cho nền tảng để ngăn việc sắp xếp lại các hướng dẫn đọc-ghi. Trên gcc, đây là

asm volatile("" ::: "memory");

( Thông tin thêm tại đây )

Lưu ý rằng điều này chỉ gián tiếp ngăn chặn các hoạt động sắp xếp lại thứ tự, miễn là chúng phụ thuộc vào các lần đọc / ghi.

Trong thực tế, tôi chưa thấy một hệ thống nào mà hệ thống gọi vào Clock::now()có tác dụng tương tự như một rào cản như vậy. Bạn có thể kiểm tra kết quả lắp ráp để chắc chắn.

Tuy nhiên, không có gì lạ khi hàm đang được kiểm tra được đánh giá trong thời gian biên dịch. Để thực thi thực thi "thực tế", bạn có thể cần lấy dữ liệu đầu vào cho foo()từ I / O hoặc volatileđọc.


Một tùy chọn khác sẽ là vô hiệu hóa nội tuyến vì foo()- một lần nữa, đây là trình biên dịch cụ thể và thường không di động, nhưng sẽ có tác dụng tương tự.

Trên gcc, đây sẽ là __attribute__ ((noinline))


@Ruslan đưa ra một vấn đề cơ bản: Phép đo này thực tế như thế nào?

Thời gian thực thi bị ảnh hưởng bởi nhiều yếu tố: một là phần cứng thực tế mà chúng ta đang chạy, hai là truy cập đồng thời vào các tài nguyên được chia sẻ như bộ nhớ cache, bộ nhớ, đĩa và lõi CPU.

Vì vậy, những gì chúng tôi thường làm để có được thời gian có thể so sánh được : đảm bảo rằng chúng có thể lặp lại với biên độ lỗi thấp. Điều này làm cho chúng có phần giả tạo.

Hiệu suất thực thi "hot cache" so với "cold cache" có thể dễ dàng khác nhau theo thứ tự cường độ - nhưng trên thực tế, nó sẽ là một cái gì đó ở giữa ("lukewarm"?)


2
Việc bạn hack với asmảnh hưởng đến thời gian thực thi các câu lệnh giữa các lần gọi bộ đếm thời gian: mã sau khi bộ nhớ đệm phải tải lại tất cả các biến từ bộ nhớ.
Ruslan

@Ruslan: Hack của họ, không phải của tôi. Có nhiều mức độ thanh lọc khác nhau và việc làm như vậy là không thể tránh khỏi để có kết quả tái tạo.
peterchen

2
Lưu ý rằng việc hack với 'asm' chỉ giúp làm rào cản cho các hoạt động chạm vào bộ nhớ và OP quan tâm đến nhiều hơn thế. Xem câu trả lời của tôi để biết thêm chi tiết.
Chandler Carruth

11

Ngôn ngữ C ++ định nghĩa những gì có thể quan sát được theo một số cách.

Nếu foo()không có gì có thể quan sát được, thì nó có thể bị loại bỏ hoàn toàn. Nếu foo()chỉ một phép tính lưu trữ các giá trị ở trạng thái "cục bộ" (có thể là trên ngăn xếp hoặc trong một đối tượng ở đâu đó) trình biên dịch có thể chứng minh rằng không có con trỏ dẫn xuất an toàn nào có thể xâm nhập vào Clock::now()mã, thì không có hậu quả nào có thể quan sát được đối với di chuyển các Clock::now()cuộc gọi.

Nếu foo()tương tác với một tập tin hoặc màn hình hiển thị, và trình biên dịch không thể chứng minh rằng Clock::now()không không tương tác với các tập tin hoặc màn hình, sau đó sắp xếp lại không thể thực hiện, bởi vì sự tương tác với một tập tin hoặc hiển thị là hành vi quan sát được.

Mặc dù bạn có thể sử dụng các thủ thuật dành riêng cho trình biên dịch để buộc mã không di chuyển xung quanh (như lắp ráp nội tuyến), một cách tiếp cận khác là cố gắng thông minh hơn trình biên dịch của bạn.

Tạo một thư viện được tải động. Tải nó trước khi mã được đề cập.

Thư viện đó cho thấy một điều:

namespace details {
  void execute( void(*)(void*), void *);
}

và kết thúc nó như thế này:

template<class F>
void execute( F f ) {
  struct bundle_t {
    F f;
  } bundle = {std::forward<F>(f)};

  auto tmp_f = [](void* ptr)->void {
    auto* pb = static_cast<bundle_t*>(ptr);
    (pb->f)();
  };
  details::execute( tmp_f, &bundle );
}

đóng gói lambda nullary và sử dụng thư viện động để chạy nó trong ngữ cảnh mà trình biên dịch không thể hiểu được.

Bên trong thư viện động, chúng tôi thực hiện:

void details::execute( void(*f)(void*), void *p) {
  f(p);
}

khá đơn giản.

Bây giờ để sắp xếp lại các lệnh gọi execute, nó phải hiểu thư viện động, thứ mà nó không thể trong khi biên dịch mã thử nghiệm của bạn.

Nó vẫn có thể loại bỏ foo()s mà không có tác dụng phụ, nhưng bạn thắng một số, bạn thua một số.


19
"một cách tiếp cận khác là cố gắng thông minh hơn trình biên dịch của bạn" Nếu cụm từ đó không phải là dấu hiệu của việc bạn đã đi xuống lỗ thỏ, tôi không biết đó là gì. :-)
Cody Grey

1
Tôi nghĩ có thể hữu ích khi lưu ý rằng thời gian cần thiết để một khối mã thực thi không được coi là một hành vi "có thể quan sát được" mà các trình biên dịch bắt buộc phải duy trì . Nếu thời gian để thực thi một khối mã là "có thể quan sát được", thì không có hình thức tối ưu hóa hiệu suất nào được phép. Mặc dù sẽ rất hữu ích đối với C và C ++ nếu xác định một "rào cản quan hệ nhân quả" sẽ yêu cầu trình biên dịch ngừng thực thi bất kỳ mã nào sau rào cản cho đến khi tất cả các tác dụng phụ từ trước khi rào cản được xử lý bởi mã được tạo [mã nào muốn đảm bảo dữ liệu có đầy đủ ...
supercat

1
... được truyền thông qua bộ nhớ đệm phần cứng sẽ cần sử dụng các phương tiện dành riêng cho phần cứng để làm điều đó, nhưng phương tiện dành riêng cho phần cứng là đợi cho đến khi tất cả các lần ghi được đăng hoàn tất sẽ vô dụng nếu không có chỉ thị rào cản để đảm bảo rằng tất cả các lần ghi đang chờ xử lý được trình biên dịch theo dõi phải được đăng lên phần cứng trước khi phần cứng được yêu cầu để đảm bảo rằng tất cả các lần ghi đã đăng đều hoàn tất.] Tôi không biết có cách nào để làm điều đó bằng một trong hai ngôn ngữ mà không sử dụng volatilequyền truy cập giả hoặc lệnh gọi đến mã bên ngoài.
supercat

4

Không, không thể. Theo tiêu chuẩn C ++ [intro.execution]:

14 Mọi phép tính giá trị và hiệu ứng phụ được liên kết với một biểu thức đầy đủ được sắp xếp theo trình tự trước khi mọi phép tính giá trị và tác dụng phụ liên kết với biểu thức đầy đủ tiếp theo được đánh giá.

Một biểu thức đầy đủ về cơ bản là một câu lệnh được kết thúc bằng dấu chấm phẩy. Như bạn có thể thấy quy tắc trên quy định các câu lệnh phải được thực hiện theo thứ tự. Đó là trong tuyên bố rằng trình biên dịch được phép tự do hoàn toàn hơn (tức là nó đang được một số tình huống cho phép để đánh giá biểu thức tạo nên một tuyên bố trong đơn đặt hàng khác ngoài trái sang phải hoặc bất cứ thứ gì cụ thể).

Lưu ý rằng các điều kiện để áp dụng quy tắc as-if không được đáp ứng ở đây. Không hợp lý khi nghĩ rằng bất kỳ trình biên dịch nào cũng có thể chứng minh rằng việc sắp xếp lại các lệnh gọi để lấy thời gian hệ thống sẽ không ảnh hưởng đến hành vi chương trình có thể quan sát được. Nếu có một trường hợp nào đó trong đó hai lệnh gọi để tính thời gian có thể được sắp xếp lại mà không thay đổi hành vi quan sát được, thì việc thực sự tạo ra một trình biên dịch phân tích một chương trình với đủ hiểu biết để có thể suy ra điều này một cách chắc chắn là vô cùng kém hiệu quả.


12
Vẫn còn như-nếu quy tắc mặc dù
MM

18
Bởi trình biên dịch quy tắc as-if có thể làm bất cứ điều gì để viết mã miễn là nó không thay đổi hành vi có thể quan sát được. Thời gian thực hiện không thể quan sát được. Vì vậy, nó có thể sắp xếp lại các dòng arbutrary mã miễn là kết quả sẽ là như nhau (hầu hết các trình biên dịch làm điều hợp lý và các cuộc gọi thời gian không sắp xếp lại, nhưng nó không phải là cần thiết)
Revolver_Ocelot

6
Thời gian thực hiện không thể quan sát được. Điều này khá lạ. Từ quan điểm thực tế, phi kỹ thuật, thời gian thực hiện (hay còn gọi là "hiệu suất") là rất dễ quan sát.
Frédéric Hamidi

3
Phụ thuộc vào cách bạn đo thời gian. Không thể đo số chu kỳ đồng hồ được thực hiện để thực thi một số nội dung mã trong C ++ chuẩn.
Peter

3
@dba Bạn đang trộn một vài thứ với nhau. Trình liên kết không còn có thể tạo ứng dụng Win16 nữa, điều đó đúng, nhưng đó là bởi vì họ đã loại bỏ hỗ trợ tạo loại nhị phân đó. Các ứng dụng WIn16 không sử dụng định dạng PE. Điều đó không có nghĩa là trình biên dịch hoặc trình liên kết có kiến ​​thức đặc biệt về các hàm API. Vấn đề khác liên quan đến thư viện thời gian chạy. Hoàn toàn không có vấn đề gì khi lấy phiên bản MSVC mới nhất để tạo tệp nhị phân chạy trên NT 4. Tôi đã làm được. Sự cố xảy ra ngay sau khi bạn cố gắng liên kết trong CRT, nó gọi các hàm không khả dụng.
Cody Grey

2

Không.

Đôi khi, theo quy tắc "as-if", các câu lệnh có thể được sắp xếp lại. Điều này không phải vì chúng độc lập với nhau về mặt logic, mà bởi vì sự độc lập đó cho phép sự sắp xếp lại như vậy xảy ra mà không làm thay đổi ngữ nghĩa của chương trình.

Di chuyển một lệnh gọi hệ thống lấy thời gian hiện tại rõ ràng là không thỏa mãn điều kiện đó. Một trình biên dịch cố ý hoặc vô tình làm như vậy là không tuân thủ và thực sự ngớ ngẩn.

Nói chung, tôi sẽ không mong đợi bất kỳ biểu thức nào dẫn đến lời gọi hệ thống là "đoán thứ hai" bởi ngay cả một trình biên dịch tối ưu hóa tích cực. Nó chỉ không biết đủ về những gì mà lệnh gọi hệ thống làm.


5
Tôi đồng ý rằng nó sẽ là ngớ ngẩn, nhưng tôi không muốn gọi nó là không phù hợp . Trình biên dịch có thể biết chính xác lệnh gọi của hệ thống trên hệ thống cụ thể là gì và nó có tác dụng phụ hay không. Tôi hy vọng các trình biên dịch sẽ không sắp xếp lại thứ tự cuộc gọi như vậy chỉ để bao gồm các trường hợp sử dụng phổ biến, cho phép trải nghiệm người dùng tốt hơn, không phải vì tiêu chuẩn cấm nó.
Revolver_Ocelot

4
@Revolver_Ocelot: Các tối ưu làm thay đổi ngữ nghĩa của chương trình (không sao, lưu để xử lý sao chép) không tuân thủ tiêu chuẩn, cho dù bạn có đồng ý hay không.
Các cuộc đua ánh sáng trong quỹ đạo vào

6
Trong trường hợp nhỏ int x = 0; clock(); x = y*2; clock();, không có cách xác định nào để clock()mã tương tác với trạng thái của x. Theo tiêu chuẩn C ++, nó không cần phải biết điều gì clock()- nó có thể kiểm tra ngăn xếp (và thông báo khi tính toán xảy ra), nhưng đó không phải là vấn đề của C ++ .
Yakk - Adam Nevraumont

5
Nói thêm về quan điểm của Yakk: đúng là việc sắp xếp lại các lệnh gọi hệ thống, để kết quả của lệnh đầu tiên được gán cho t2và kết quả thứ hai cho t1, sẽ không phù hợp và ngớ ngẩn nếu những giá trị đó được sử dụng, câu trả lời này bỏ sót là một trình biên dịch phù hợp đôi khi có thể sắp xếp lại mã khác trong một lệnh gọi hệ thống. Trong trường hợp này, với điều kiện là nó biết những gì foo()sẽ làm (ví dụ vì nó đã nội dung nó) và do đó (nói một cách lỏng lẻo) đó là một hàm thuần túy thì nó có thể di chuyển nó xung quanh.
Steve Jessop

1
.. nói một cách lỏng lẻo, điều này là do không có gì đảm bảo rằng việc triển khai thực tế (mặc dù không phải là máy trừu tượng) sẽ không tính toán đầu cơ y*ytrước khi hệ thống gọi, chỉ cho vui thôi. Cũng không có gì đảm bảo rằng việc triển khai thực tế sẽ không sử dụng kết quả của phép tính suy đoán này sau này tại bất kỳ thời điểm nào xđược sử dụng, do đó không phải làm gì giữa các lần gọi tới clock(). Điều tương tự cũng xảy ra với bất kỳ chức năng nội tuyến foonào, miễn là nó không có tác dụng phụ và không thể phụ thuộc vào trạng thái có thể bị thay đổi clock().
Steve Jessop

0

noinline hàm + hộp đen lắp ráp nội tuyến + toàn bộ dữ liệu phụ thuộc

Điều này dựa trên https://stackoverflow.com/a/38025837/895245 nhưng vì tôi không thấy bất kỳ lời giải thích rõ ràng nào về việc tại sao ::now()không thể sắp xếp lại thứ tự ở đó, tôi thà hoang tưởng và đặt nó vào bên trong một hàm noinline cùng với asm.

Bằng cách này, tôi khá chắc chắn rằng việc sắp xếp lại thứ tự không thể xảy ra, vì noinline"ràng buộc" sự ::nowphụ thuộc dữ liệu và.

main.cpp

#include <chrono>
#include <iostream>
#include <string>

// noinline ensures that the ::now() cannot be split from the __asm__
template <class T>
__attribute__((noinline)) auto get_clock(T& value) {
    // Make the compiler think we actually use / modify the value.
    // It can't "see" what is going on inside the assembly string.
    __asm__ __volatile__ ("" : "+g" (value));
    return std::chrono::high_resolution_clock::now();
}

template <class T>
static T foo(T niters) {
    T result = 42;
    for (T i = 0; i < niters; ++i) {
        result = (result * result) - (3 * result) + 1;
    }
    return result;
}

int main(int argc, char **argv) {
    unsigned long long input;
    if (argc > 1) {
        input = std::stoull(argv[1], NULL, 0);
    } else {
        input = 1;
    }

    // Must come before because it could modify input
    // which is passed as a reference.
    auto t1 = get_clock(input);
    auto output = foo(input);
    // Must come after as it could use the output.
    auto t2 = get_clock(output);
    std::cout << "output " << output << std::endl;
    std::cout << "time (ns) "
              << std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count()
              << std::endl;
}

GitHub ngược dòng .

Biên dịch và chạy:

g++ -ggdb3 -O3 -std=c++14 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out 1000
./main.out 10000
./main.out 100000

Nhược điểm nhỏ duy nhất của phương pháp này là chúng ta thêm một callqlệnh bổ sung vào một inlinephương thức. objdump -CDhiển thị maincó chứa:

    11b5:       e8 26 03 00 00          callq  14e0 <auto get_clock<unsigned long long>(unsigned long long&)>
    11ba:       48 8b 34 24             mov    (%rsp),%rsi
    11be:       48 89 c5                mov    %rax,%rbp
    11c1:       b8 2a 00 00 00          mov    $0x2a,%eax
    11c6:       48 85 f6                test   %rsi,%rsi
    11c9:       74 1a                   je     11e5 <main+0x65>
    11cb:       31 d2                   xor    %edx,%edx
    11cd:       0f 1f 00                nopl   (%rax)
    11d0:       48 8d 48 fd             lea    -0x3(%rax),%rcx
    11d4:       48 83 c2 01             add    $0x1,%rdx
    11d8:       48 0f af c1             imul   %rcx,%rax
    11dc:       48 83 c0 01             add    $0x1,%rax
    11e0:       48 39 d6                cmp    %rdx,%rsi
    11e3:       75 eb                   jne    11d0 <main+0x50>
    11e5:       48 89 df                mov    %rbx,%rdi
    11e8:       48 89 44 24 08          mov    %rax,0x8(%rsp)
    11ed:       e8 ee 02 00 00          callq  14e0 <auto get_clock<unsigned long long>(unsigned long long&)>

vì vậy chúng tôi thấy rằng nó foođã được nội tuyến, nhưng get_clockkhông phải và bao quanh nó.

get_clock Tuy nhiên, bản thân nó cực kỳ hiệu quả, bao gồm một lệnh được tối ưu hóa cuộc gọi đơn lẻ mà thậm chí không chạm vào ngăn xếp:

00000000000014e0 <auto get_clock<unsigned long long>(unsigned long long&)>:
    14e0:       e9 5b fb ff ff          jmpq   1040 <std::chrono::_V2::system_clock::now()@plt>

Vì bản thân độ chính xác của đồng hồ bị giới hạn, tôi nghĩ rằng bạn sẽ không thể nhận thấy tác động thời gian của một số phụ jmpq. Lưu ý rằng một cái calllà bắt buộc bất kể ::now()nó nằm trong thư viện dùng chung.

Gọi ::now()từ hợp ngữ nội tuyến với một phụ thuộc dữ liệu

Đây sẽ là giải pháp hiệu quả nhất có thể, khắc phục ngay cả những điều bổ sung jmpqđược đề cập ở trên.

Rất tiếc, điều này cực kỳ khó thực hiện chính xác như được hiển thị tại: Gọi printf trong ASM nội tuyến mở rộng

Tuy nhiên, nếu phép đo thời gian của bạn có thể được thực hiện trực tiếp trong lắp ráp nội tuyến mà không cần gọi, thì kỹ thuật này có thể được sử dụng. Đây là trường hợp ví dụ cho các hướng dẫn thiết bị ma thuật gem5 , x86 RDTSC (không chắc liệu đây có phải là đại diện hay không) và có thể là các bộ đếm hiệu suất khác.

Chủ đề liên quan:

Đã thử nghiệm với GCC 8.3.0, Ubuntu 19.04.


1
Thông thường, bạn không cần phải ép tràn / tải lại "+m", sử dụng "+r"là một cách hiệu quả hơn nhiều để làm cho trình biên dịch hiện thực hóa một giá trị và sau đó giả sử biến đã thay đổi.
Peter Cordes
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.