2012-06-28 32 views
13

Tôi đang thử nghiệm với một lexer, và tôi thấy rằng việc chuyển từ một vòng lặp while sang câu lệnh if và một vòng lặp while trong một phần của chương trình dẫn đến mã nhanh hơn ~ 20%, điều này có vẻ điên rồ. Tôi đã cô lập sự khác biệt trong trình biên dịch tạo ra mã cho các đoạn mã lắp ráp này. Có ai biết tại sao mã nhanh nhanh hơn không?Tại sao mã lắp ráp này nhanh hơn?

Trong assembly, 'edi' là vị trí văn bản hiện tại, 'ebx' là phần cuối của văn bản và 'isAlpha' là bảng tra cứu có 1 nếu ký tự là chữ cái và 0 nếu không.

Mã chậm:

slow_loop: 
00401897 cmp edi,ebx 
00401899 je slow_done (4018AAh) 
0040189B movzx eax,byte ptr [edi] 
0040189E cmp byte ptr isAlpha (4533E0h)[eax],0 
004018A5 je slow_done (4018AAh) 
004018A7 inc edi 
004018A8 jmp slow_loop (401897h) 
slow_done: 

Mã nhanh:

fast_loop: 
0040193D inc edi 
0040193E cmp edi,ebx 
00401940 je fast_done (40194Eh) 
00401942 movzx eax,byte ptr [edi] 
00401945 cmp byte ptr isAlpha (4533E0h)[eax],0 
0040194C jne fast_loop (40193Dh) 
fast_done: 

Nếu tôi chỉ chạy những đoạn lắp ráp chống lại một megabyte của văn bản bao gồm duy nhất của chữ 'a', mã nhanh nhanh hơn 30%. Đoán của tôi là mã chậm chạp chậm vì lỗi chi nhánh, nhưng tôi nghĩ trong một vòng lặp là chi phí một lần.

Đây là chương trình mà tôi sử dụng để kiểm tra cả hai đoạn:

#include <Windows.h> 
#include <string> 
#include <iostream> 

int main(int argc, char* argv[]) 
{ 
    static char isAlpha[256]; 
    for (int i = 0; i < sizeof(isAlpha); ++i) 
     isAlpha[i] = isalpha(i) ? 1 : 0; 

    std::string test(1024*1024, 'a'); 

    const char* start = test.c_str(); 
    const char* limit = test.c_str() + test.size(); 

    DWORD slowStart = GetTickCount(); 
    for (int i = 0; i < 10000; ++i) 
    { 
     __asm 
     { 
      mov edi, start 
      mov ebx, limit 

      inc edi 

     slow_loop: 
      cmp edi,ebx 
      je slow_done 
      movzx eax,byte ptr [edi] 
      cmp byte ptr isAlpha [eax],0 
      je slow_done 
      inc edi 
      jmp slow_loop 

     slow_done: 
     } 
    } 
    DWORD slowEnd = GetTickCount(); 
    std::cout << "slow in " << (slowEnd - slowStart) << " ticks" << std::endl; 

    DWORD fastStart = GetTickCount(); 
    for (int i = 0; i < 10000; ++i) 
    { 
     __asm 
     { 
      mov edi, start 
      mov ebx, limit 

     fast_loop: 
      inc edi 
      cmp edi,ebx 
      je fast_done 
      movzx eax,byte ptr [edi] 
      cmp byte ptr isAlpha [eax],0 
      jne fast_loop 

     fast_done: 
     } 
    } 
    DWORD fastEnd = GetTickCount(); 
    std::cout << "fast in " << (fastEnd - fastStart) << " ticks" << std::endl; 

    return 0; 
} 

Đầu ra của chương trình thử nghiệm là

slow in 8455 ticks 
fast in 5694 ticks 
+4

Điều đó * là * điên - đó là một sự tối ưu hóa rất phổ biến cho các trình biên dịch tự làm. Đối với lý do tại sao nó nhanh hơn, có một ít nhảy cho mỗi lần lặp lại trong mã nhanh, và nhảy chỉ có một thông lượng giới hạn. – harold

+2

trình biên dịch đăng ký dựa trên hiệu suất có thể mang lại câu trả lời tốt nhất, nhưng ngoài bước nhảy rõ ràng, tôi đoán bit thứ hai nhanh hơn vì nó phù hợp hơn vào bộ đệm mã (cũng có vài byte để tìm nạp và giải mã, nhưng đó là vô nghĩa ở đây). liên kết mục tiêu nhảy cũng có thể là một yếu tố khác, nhưng điều đó khó nói ở đây mà không có địa chỉ – Necrolis

+0

Brian, CPU của bạn là gì? Hãy xem http://www.agner.org/optimize/ Và harold, các jmps tĩnh được dự đoán là luôn được thực hiện trên các CPU x86 hiện đại (không phải Atom), vì vậy chúng không tốn chi phí. – osgx

Trả lời

11

Xin lỗi, tôi không thể tái tạo mã của bạn chính xác trên GCC (linux), nhưng tôi có một số kết quả và tôi nghĩ rằng ý tưởng chính đã được lưu trong mã của tôi.

Có một công cụ từ Intel để phân tích hiệu suất đoạn mã: http://software.intel.com/en-us/articles/intel-architecture-code-analyzer/ (Intel IACA). Nó là miễn phí để tải về và kiểm tra nó.

Trong thí nghiệm của tôi, báo cáo cho vòng lặp chậm:

Intel(R) Architecture Code Analyzer Version - 2.0.1 
Analyzed File - ./l2_i 
Binary Format - 32Bit 
Architecture - SNB 
Analysis Type - Throughput 

Throughput Analysis Report 
-------------------------- 
Block Throughput: 3.05 Cycles  Throughput Bottleneck: Port5 

Port Binding In Cycles Per Iteration: 
------------------------------------------------------------------------- 
| Port | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 
------------------------------------------------------------------------- 
| Cycles | 0.5 0.0 | 0.5 | 1.0 1.0 | 1.0 1.0 | 0.0 | 3.0 | 
------------------------------------------------------------------------- 

N - port number or number of cycles resource conflict caused delay, DV - Divide 
D - Data fetch pipe (on ports 2 and 3), CP - on a critical path 
F - Macro Fusion with the previous instruction occurred 

| Num Of |    Ports pressure in cycles    | | 
| Uops | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | | 
--------------------------------------------------------------------- 
| 1 |   |  |   |   |  | 1.0 | CP | cmp edi, 
| 0F |   |  |   |   |  |  | | jz 0xb 
| 1 |   |  | 1.0 1.0 |   |  |  | | movzx ebx 
| 2 |   |  |   | 1.0 1.0 |  | 1.0 | CP | cmp cl, b 
| 0F |   |  |   |   |  |  | | jz 0x3 
| 1 | 0.5  | 0.5 |   |   |  |  | | inc edi 
| 1 |   |  |   |   |  | 1.0 | CP | jmp 0xfff 

Đối với loop nhanh:

Throughput Analysis Report 
-------------------------- 
Block Throughput: 2.00 Cycles  Throughput Bottleneck: Port5 

Port Binding In Cycles Per Iteration: 
------------------------------------------------------------------------- 
| Port | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 
------------------------------------------------------------------------- 
| Cycles | 0.5 0.0 | 0.5 | 1.0 1.0 | 1.0 1.0 | 0.0 | 2.0 | 
------------------------------------------------------------------------- 

N - port number or number of cycles resource conflict caused delay, DV - Divide 
D - Data fetch pipe (on ports 2 and 3), CP - on a critical path 
F - Macro Fusion with the previous instruction occurred 

| Num Of |    Ports pressure in cycles    | | 
| Uops | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | | 
--------------------------------------------------------------------- 
| 1 | 0.5  | 0.5 |   |   |  |  | | inc edi 
| 1 |   |  |   |   |  | 1.0 | CP | cmp edi, 
| 0F |   |  |   |   |  |  | | jz 0x8 
| 1 |   |  | 1.0 1.0 |   |  |  | | movzx ebx 
| 2 |   |  |   | 1.0 1.0 |  | 1.0 | CP | cmp cl, b 
| 0F |   |  |   |   |  |  | | jnz 0xfff 

Vì vậy, trong vòng chậm JMP là một hướng dẫn thêm trong đường dẫn quan trọng. Tất cả các cặp cmp + jz/jnz được hợp nhất (Macro-fusion) thành u-op đơn. Và trong việc thực thi mã của tôi, tài nguyên quan trọng là Port5, có thể thực thi ALU + JMP (và nó là cổng duy nhất có khả năng JMP).

PS: Nếu ai đó không có ý tưởng về nơi đặt cổng, có hình ảnh firstsecond; và bài viết: rwt

PPS: IACA có một số hạn chế; nó chỉ mô hình hóa một số phần của CPU (Execution units), và không ghi nhớ cache, lỗi chi nhánh, các hình phạt khác nhau, thay đổi tần số/công suất, ngắt hệ điều hành, tranh chấp HyperThreading cho các đơn vị Execution và nhiều hiệu ứng khác. Nhưng nó là công cụ hữu ích vì nó có thể cung cấp cho bạn một số cái nhìn nhanh chóng bên trong lõi nội bộ nhất của CPU Intel hiện đại. Và nó chỉ hoạt động cho các vòng bên trong (giống như các vòng trong câu hỏi này).

+0

Vì vậy, do làm thế nào các hướng dẫn có thể được lên kế hoạch như micro-ops, vòng lặp chậm mất 3.05 chu kỳ để thực thi và vòng lặp nhanh có 2 chu kỳ. Đó là lý do tại sao có sự khác biệt lớn giữa thời gian thực hiện của họ mặc dù chỉ có thêm 1 lệnh trong vòng lặp chậm. Có đúng không? – briangreenery

+0

IACA là trình giả lập và không thể hoàn toàn chính xác. 'Khối thông lượng:' được tính cho một số trường hợp lý tưởng (ví dụ: trong vòng lặp vô hạn không có bộ nhớ đệm) và đối với một số mô hình CPU (không chính xác). Công cụ này có thể ước tính rằng trong trường hợp tốt nhất sẽ có nút cổ chai trong Đơn vị thi hành số 5 (Port5) - và thời gian tối thiểu cho một lần lặp là 3 hoặc 2 chu kỳ đồng hồ. Nó có thể được tính toán bởi bất kỳ ai có kiến ​​thức về bản dịch lệnh cơ bản cho các vi lệnh và biết phần cứng theo yêu cầu của các hướng dẫn JE/JNE/JMP. – osgx

+0

Hướng dẫn bổ sung này làm cho đường dẫn quan trọng dài hơn, vì vậy nó sẽ ảnh hưởng đến trường hợp tốt nhất. Cảm ơn bạn cho câu hỏi thú vị! – osgx

2

văn bản thử nghiệm của bạn gây ra vòng lặp để chạy đến khi kết thúc cho mỗi và mọi lặp lại và vòng lặp nhanh có ít lệnh hơn.

+0

Đột nhiên, bạn cũng đúng. – osgx

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