Là ngôn ngữ lắp ráp nội tuyến chậm hơn mã C ++ bản địa?


182

Tôi đã cố gắng so sánh hiệu suất của ngôn ngữ lắp ráp nội tuyến và mã C ++, vì vậy tôi đã viết một hàm thêm hai mảng có kích thước 2000 cho 100000 lần. Đây là mã:

#define TIMES 100000
void calcuC(int *x,int *y,int length)
{
    for(int i = 0; i < TIMES; i++)
    {
        for(int j = 0; j < length; j++)
            x[j] += y[j];
    }
}


void calcuAsm(int *x,int *y,int lengthOfArray)
{
    __asm
    {
        mov edi,TIMES
        start:
        mov esi,0
        mov ecx,lengthOfArray
        label:
        mov edx,x
        push edx
        mov eax,DWORD PTR [edx + esi*4]
        mov edx,y
        mov ebx,DWORD PTR [edx + esi*4]
        add eax,ebx
        pop edx
        mov [edx + esi*4],eax
        inc esi
        loop label
        dec edi
        cmp edi,0
        jnz start
    };
}

Đây là main():

int main() {
    bool errorOccured = false;
    setbuf(stdout,NULL);
    int *xC,*xAsm,*yC,*yAsm;
    xC = new int[2000];
    xAsm = new int[2000];
    yC = new int[2000];
    yAsm = new int[2000];
    for(int i = 0; i < 2000; i++)
    {
        xC[i] = 0;
        xAsm[i] = 0;
        yC[i] = i;
        yAsm[i] = i;
    }
    time_t start = clock();
    calcuC(xC,yC,2000);

    //    calcuAsm(xAsm,yAsm,2000);
    //    for(int i = 0; i < 2000; i++)
    //    {
    //        if(xC[i] != xAsm[i])
    //        {
    //            cout<<"xC["<<i<<"]="<<xC[i]<<" "<<"xAsm["<<i<<"]="<<xAsm[i]<<endl;
    //            errorOccured = true;
    //            break;
    //        }
    //    }
    //    if(errorOccured)
    //        cout<<"Error occurs!"<<endl;
    //    else
    //        cout<<"Works fine!"<<endl;

    time_t end = clock();

    //    cout<<"time = "<<(float)(end - start) / CLOCKS_PER_SEC<<"\n";

    cout<<"time = "<<end - start<<endl;
    return 0;
}

Sau đó, tôi chạy chương trình năm lần để có được các chu kỳ của bộ xử lý, có thể được xem là thời gian. Mỗi lần tôi gọi một trong các chức năng được đề cập ở trên.

Và đây là kết quả.

Chức năng của phiên bản lắp ráp:

Debug   Release
---------------
732        668
733        680
659        672
667        675
684        694
Average:   677

Chức năng của phiên bản C ++:

Debug     Release
-----------------
1068      168
 999      166
1072      231
1002      166
1114      183
Average:  182

Mã C ++ trong chế độ phát hành nhanh hơn gần 3,7 lần so với mã lắp ráp. Tại sao?

Tôi đoán rằng mã lắp ráp tôi đã viết không hiệu quả như mã được tạo bởi GCC. Thật khó để một lập trình viên thông thường như tôi viết mã nhanh hơn đối thủ do trình biên dịch tạo ra. Điều đó có nghĩa là tôi không nên tin vào hiệu suất của ngôn ngữ hợp ngữ được viết bởi tay mình, tập trung vào C ++ và quên ngôn ngữ hợp ngữ?


29
Khá nhiều. Lắp ráp bằng tay là phù hợp trong một số trường hợp, nhưng phải cẩn thận để đảm bảo rằng phiên bản lắp ráp thực sự nhanh hơn những gì có thể đạt được với ngôn ngữ cấp cao hơn.
Magnus Hoff

161
Bạn có thể thấy việc hướng dẫn nghiên cứu mã do trình biên dịch tạo ra và cố gắng hiểu tại sao nó nhanh hơn phiên bản lắp ráp của bạn.
Paul R

34
Vâng, có vẻ như trình biên dịch tốt hơn trong việc viết asm hơn bạn. Trình biên dịch hiện đại thực sự là khá tốt.
David Heffernan

20
Bạn đã nhìn vào lắp ráp GCC sản xuất? GCC có thể sử dụng hướng dẫn MMX. Hàm của bạn rất song song - bạn có khả năng có thể sử dụng bộ xử lý N để tính tổng trong 1 / N lần. Hãy thử một chức năng mà không có hy vọng cho sự song song.
Chris

11
Hừm, tôi đã mong đợi một trình biên dịch tốt để thực hiện việc này nhanh hơn ~ 100000 lần ...
PlasmaHH

Câu trả lời:


260

Vâng, hầu hết các lần.

Trước hết, bạn bắt đầu từ giả định sai rằng ngôn ngữ cấp thấp (lắp ráp trong trường hợp này) sẽ luôn tạo mã nhanh hơn ngôn ngữ cấp cao (C ++ và C trong trường hợp này). Nó không đúng. Có phải mã C luôn nhanh hơn mã Java? Không bởi vì có một biến khác: lập trình viên. Cách bạn viết mã và kiến ​​thức về chi tiết kiến ​​trúc ảnh hưởng rất lớn đến hiệu suất (như bạn đã thấy trong trường hợp này).

Bạn luôn có thể tạo một ví dụ trong đó mã lắp ráp thủ công tốt hơn mã được biên dịch nhưng thông thường đó là một ví dụ hư cấu hoặc một thói quen duy nhất không phải là một chương trình thực sự của hơn 500.000 dòng mã C ++). Tôi nghĩ rằng trình biên dịch sẽ sản xuất tốt hơn lắp ráp đang 95% thời gian và đôi khi, chỉ có một số lần hiếm hoi, bạn có thể cần phải viết lắp ráp mã cho số ít, ngắn, sử dụng cao , hiệu suất quan trọng thói quen hoặc khi bạn phải truy cập các tính năng ngôn ngữ cao cấp ưa thích của bạn không phơi bày. Bạn có muốn một liên lạc của sự phức tạp này? Đọc câu trả lời tuyệt vời này ở đây trên SO.

Tại sao là cái này?

Trước hết vì trình biên dịch có thể thực hiện tối ưu hóa mà chúng ta thậm chí không thể tưởng tượng được (xem danh sách ngắn này ) và chúng sẽ thực hiện chúng trong vài giây (khi chúng ta có thể cần ngày ).

Khi bạn mã trong lắp ráp, bạn phải thực hiện các chức năng được xác định rõ với giao diện cuộc gọi được xác định rõ. Tuy nhiên, họ có thể tính đến tối ưu hóa toàn bộ chương trìnhtối ưu hóa liên thủ tục như phân bổ đăng ký , truyền bá liên tục , loại bỏ biểu hiện chung , lập lịch hướng dẫn và tối ưu hóa phức tạp khác, chẳng hạn ( mô hình Polytope chẳng hạn). Trên kiến trúc RISC, các chàng trai đã ngừng lo lắng về điều này từ nhiều năm trước (ví dụ, lập lịch hướng dẫn, rất khó điều chỉnh bằng tay ) và CPU CISC hiện đại có các đường ống rất dài quá.

Đối với một số bộ vi điều khiển phức tạp, thậm chí các thư viện hệ thống được viết bằng C thay vì lắp ráp vì trình biên dịch của chúng tạo ra mã cuối cùng tốt hơn (và dễ bảo trì).

Trình biên dịch đôi khi có thể tự động sử dụng một số hướng dẫn MMX / SIMDx và nếu bạn không sử dụng chúng, bạn chỉ không thể so sánh (các câu trả lời khác đã xem xét mã lắp ráp của bạn rất tốt). Chỉ với các vòng lặp, đây là một danh sách ngắn các tối ưu hóa vòng lặp của trình biên dịch thường được kiểm tra (bạn có nghĩ rằng bạn có thể tự làm điều đó khi lịch trình của bạn đã được quyết định cho chương trình C # không?) Nếu bạn viết một cái gì đó trong lắp ráp, tôi nghĩ rằng bạn phải xem xét ít nhất một số tối ưu hóa đơn giản . Ví dụ về sách học cho các mảng là để hủy bỏ chu trình (kích thước của nó được biết đến tại thời điểm biên dịch). Làm điều đó và chạy thử nghiệm của bạn một lần nữa.

Ngày nay, nó thực sự không phổ biến khi cần sử dụng ngôn ngữ lắp ráp vì một lý do khác: rất nhiều CPU khác nhau . Bạn có muốn hỗ trợ tất cả? Mỗi có một kiến trúc vi mô cụ thể và một số bộ hướng dẫn cụ thể . Chúng có số lượng đơn vị chức năng khác nhau và hướng dẫn lắp ráp nên được sắp xếp để giữ cho tất cả chúng bận rộn . Nếu bạn viết bằng C, bạn có thể sử dụng PGO nhưng khi lắp ráp, bạn sẽ cần một kiến ​​thức tuyệt vời về kiến ​​trúc cụ thể đó (và suy nghĩ lại và làm lại mọi thứ cho kiến ​​trúc khác ). Đối với các tác vụ nhỏ, trình biên dịch thường làm việc đó tốt hơn và đối với các tác vụ phức tạp thường thì công việc không được hoàn trả (vàtrình biên dịch có thể làm tốt hơn dù sao).

Nếu bạn ngồi xuống và xem mã của bạn, có thể bạn sẽ thấy rằng bạn sẽ nhận được nhiều hơn để thiết kế lại thuật toán của mình hơn là dịch sang lắp ráp (đọc bài đăng tuyệt vời này ở đây trên SO ), có tối ưu hóa ở mức cao (và gợi ý cho trình biên dịch) bạn có thể áp dụng hiệu quả trước khi bạn cần dùng đến ngôn ngữ lắp ráp. Có lẽ đáng để đề cập rằng thường sử dụng nội tại bạn sẽ có hiệu suất đạt được mà bạn đang tìm kiếm và trình biên dịch sẽ vẫn có thể thực hiện hầu hết các tối ưu hóa của nó.

Tất cả điều này đã nói, ngay cả khi bạn có thể sản xuất mã lắp ráp nhanh hơn 5 ~ 10 lần, bạn nên hỏi khách hàng xem họ có muốn trả một tuần thời gian của bạn hay mua CPU nhanh hơn 50 đô la . Tối ưu hóa cực kỳ thường xuyên hơn không (và đặc biệt là trong các ứng dụng LOB) đơn giản là không bắt buộc từ hầu hết chúng ta.


9
Dĩ nhiên là không. Tôi nghĩ rằng nó tốt hơn 95% của mọi người trong 99% lần. Đôi khi vì đơn giản là tốn kém (vì toán phức tạp ) hoặc chi tiêu thời gian (sau đó lại tốn kém). Đôi khi vì đơn giản là chúng ta đã quên mất việc tối ưu hóa ...
Adriano Repetti

62
@ ja72 - không, viết mã không tốt hơn . Tốt hơn hết là tối ưu hóa mã.
Mike Baranczak

14
Nó phản trực giác cho đến khi bạn thực sự xem xét nó. Theo cùng một cách, các máy dựa trên VM đang bắt đầu thực hiện tối ưu hóa thời gian chạy mà trình biên dịch đơn giản là không có thông tin để thực hiện.
Bill K

6
@ M28: Trình biên dịch có thể sử dụng các hướng dẫn tương tự. Chắc chắn, họ trả tiền cho nó theo kích thước nhị phân (vì họ phải cung cấp đường dẫn dự phòng trong trường hợp những hướng dẫn đó không được hỗ trợ). Ngoài ra, đối với hầu hết các phần, "hướng dẫn mới" sẽ được thêm vào là hướng dẫn SMID, điều mà cả VM và Trình biên dịch đều khá kinh khủng khi sử dụng. Máy ảo trả tiền cho tính năng này ở chỗ chúng phải biên dịch mã khi khởi động.
Billy ONeal

9
@BillK: PGO làm điều tương tự cho trình biên dịch.
Billy ONeal

194

Mã lắp ráp của bạn là tối ưu và có thể được cải thiện:

  • Bạn đang đẩy và bật một thanh ghi ( EDX ) trong vòng lặp bên trong của bạn. Điều này nên được di chuyển ra khỏi vòng lặp.
  • Bạn tải lại các con trỏ mảng trong mỗi lần lặp của vòng lặp. Điều này sẽ di chuyển ra khỏi vòng lặp.
  • Bạn sử dụng loophướng dẫn, được biết là đã chết chậm trên hầu hết các CPU hiện đại (có thể là kết quả của việc sử dụng sách lắp ráp cổ *)
  • Bạn không có lợi thế của unrolling vòng lặp thủ công.
  • Bạn không sử dụng các hướng dẫn SIMD có sẵn .

Vì vậy, trừ khi bạn cải thiện đáng kể bộ kỹ năng của mình về trình biên dịch chương trình, bạn không nên viết mã trình biên dịch cho hiệu năng.

* Tất nhiên tôi không biết nếu bạn thực sự có loophướng dẫn từ một cuốn sách lắp ráp cổ xưa. Nhưng bạn hầu như không bao giờ nhìn thấy nó trong mã thế giới thực, vì mọi trình biên dịch ngoài kia đủ thông minh để không phát ra loop, bạn chỉ thấy nó trong các cuốn sách xấu và lỗi thời của IMHO.


trình biên dịch vẫn có thể phát ra loop(và nhiều hướng dẫn "không dùng nữa") nếu bạn tối ưu hóa kích thước
phuclv

1
@phuclv cũng có, nhưng câu hỏi ban đầu chính xác là về tốc độ chứ không phải kích thước.
IGR94

60

Ngay cả trước khi đi sâu vào lắp ráp, có những biến đổi mã tồn tại ở mức cao hơn.

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
  for (int i = 0; i < TIMES; i++) {
    for (int j = 0; j < length; j++) {
      x[j] += y[j];
    }
  }
}

có thể được chuyển đổi thông qua Xoay vòng :

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
    for (int j = 0; j < length; ++j) {
      for (int i = 0; i < TIMES; ++i) {
        x[j] += y[j];
      }
    }
}

đó là tốt hơn nhiều như xa địa phương bộ nhớ đi.

Điều này có thể được tối ưu hóa hơn nữa, làm a += bX lần tương đương với làm a += X * bnhư vậy chúng ta nhận được:

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
    for (int j = 0; j < length; ++j) {
      x[j] += TIMES * y[j];
    }
}

tuy nhiên có vẻ như trình tối ưu hóa yêu thích của tôi (LLVM) không thực hiện chuyển đổi này.

[sửa] Tôi thấy rằng việc chuyển đổi được thực hiện nếu chúng ta có restrictvòng loại đến xy. Thật vậy, không có hạn chế này x[j]y[j]có thể bí danh đến cùng một vị trí khiến cho việc chuyển đổi này trở nên sai lầm. [kết thúc chỉnh sửa]

Dù sao, đây là, tôi nghĩ, phiên bản C được tối ưu hóa. Nó đã đơn giản hơn nhiều. Dựa trên điều này, đây là vết nứt của tôi tại ASM (Tôi để Clang tạo ra nó, tôi vô dụng với nó):

calcuAsm:                               # @calcuAsm
.Ltmp0:
    .cfi_startproc
# BB#0:
    testl   %edx, %edx
    jle .LBB0_2
    .align  16, 0x90
.LBB0_1:                                # %.lr.ph
                                        # =>This Inner Loop Header: Depth=1
    imull   $100000, (%rsi), %eax   # imm = 0x186A0
    addl    %eax, (%rdi)
    addq    $4, %rsi
    addq    $4, %rdi
    decl    %edx
    jne .LBB0_1
.LBB0_2:                                # %._crit_edge
    ret
.Ltmp1:
    .size   calcuAsm, .Ltmp1-calcuAsm
.Ltmp2:
    .cfi_endproc

Tôi e rằng tôi không hiểu tất cả các hướng dẫn đó đến từ đâu, tuy nhiên bạn luôn có thể vui vẻ và thử xem nó so sánh như thế nào ... nhưng tôi vẫn sử dụng phiên bản C được tối ưu hóa thay vì lắp ráp, theo mã, di động hơn nhiều.


Cảm ơn câu trả lời của bạn. Vâng, hơi khó hiểu khi tôi học lớp có tên "Nguyên tắc biên dịch", tôi đã học được rằng trình biên dịch sẽ tối ưu hóa mã của chúng tôi bằng nhiều cách. Điều đó có nghĩa là chúng ta cần tối ưu hóa mã theo cách thủ công? Chúng ta có thể thực hiện công việc tốt hơn trình biên dịch không? Đó là câu hỏi luôn làm tôi bối rối.
dùng957121

2
@ user957121: chúng tôi có thể tối ưu hóa nó tốt hơn khi có nhiều thông tin hơn. Cụ thể ở đây điều gây cản trở trình biên dịch là bí danh có thể có giữa xy. Đó là, các trình biên dịch không thể chắc chắn rằng cho tất cả i,jtrong [0, length)chúng ta có x + i != y + j. Nếu có sự chồng chéo, thì tối ưu hóa là không thể. Ngôn ngữ C đã giới thiệu restricttừ khóa để nói với trình biên dịch rằng hai con trỏ không thể bí danh, tuy nhiên nó không hoạt động cho các mảng vì chúng vẫn có thể trùng nhau ngay cả khi chúng không chính xác bí danh.
Matthieu M.

GCC và Clang hiện tại tự động vector hóa (sau khi kiểm tra không trùng lặp nếu bạn bỏ qua __restrict). SSE2 là cơ sở cho x86-64 và với việc xáo trộn SSE2 có thể thực hiện nhân bội 32 bit một lần (tạo ra các sản phẩm 64 bit, do đó việc xáo trộn để đưa kết quả trở lại với nhau). godbolt.org/z/r7F_uo . (SSE4.1 là cần thiết cho pmulld: đóng gói 32x32 => nhân 32 bit). GCC có một thủ thuật gọn gàng là biến các số nhân số nguyên không đổi thành shift / add (và / hoặc trừ), rất tốt cho các số nhân với một vài bit được đặt. Mã shuffle-heavy của Clang sẽ bị nghẽn cổ chai về thông lượng xáo trộn trên CPU Intel.
Peter Cordes

41

Câu trả lời ngắn gọn: có.

Câu trả lời dài: có, trừ khi bạn thực sự biết bạn đang làm gì và có lý do để làm điều đó.


3
và sau đó chỉ khi bạn chạy một công cụ định hình mức lắp ráp như vtune cho chip intel để xem nơi bạn có thể cải thiện mọi thứ
Mark Mullin

1
Điều này về mặt kỹ thuật trả lời câu hỏi nhưng cũng hoàn toàn vô dụng. A -1 từ tôi.
Navin

2
Câu trả lời rất dài: "Có, trừ khi bạn cảm thấy muốn thay đổi toàn bộ mã của mình bất cứ khi nào CPU (er) mới được sử dụng. Chọn thuật toán tốt nhất, nhưng hãy để trình biên dịch thực hiện tối ưu hóa"
Tommylee2k

35

Tôi đã sửa mã asm của mình:

  __asm
{   
    mov ebx,TIMES
 start:
    mov ecx,lengthOfArray
    mov esi,x
    shr ecx,1
    mov edi,y
label:
    movq mm0,QWORD PTR[esi]
    paddd mm0,QWORD PTR[edi]
    add edi,8
    movq QWORD PTR[esi],mm0
    add esi,8
    dec ecx 
    jnz label
    dec ebx
    jnz start
};

Kết quả cho phiên bản phát hành:

 Function of assembly version: 81
 Function of C++ version: 161

Mã lắp ráp trong chế độ phát hành nhanh hơn gần 2 lần so với C ++.


18
Bây giờ nếu bạn bắt đầu sử dụng SSE thay vì MMX (tên đăng ký là xmm0thay vì mm0), bạn sẽ nhận được một lần tăng tốc khác theo hệ số hai ;-)
Gunther Piez

8
Tôi đã thay đổi, có 41 cho phiên bản lắp ráp. Nó nhanh hơn gấp 4 lần :)
sasha

3
cũng có thể nhận thêm tới 5% nếu sử dụng tất cả các thanh ghi xmm
sasha

7
Bây giờ nếu bạn nghĩ về thời gian nó thực sự đưa bạn: lắp ráp, khoảng 10 giờ hoặc lâu hơn? C ++, tôi đoán vài phút? Có một người chiến thắng rõ ràng ở đây, trừ khi đó là mã quan trọng về hiệu suất.
Calimo

1
Một trình biên dịch tốt sẽ tự động vector hóa với paddd xmm(sau khi kiểm tra sự trùng lặp giữa xyvì bạn không sử dụng int *__restrict x). Ví dụ gcc thực hiện điều đó: godbolt.org/z/c2JG0- . Hoặc sau khi nội tuyến main, không cần kiểm tra sự chồng chéo vì nó có thể thấy sự phân bổ và chứng minh rằng chúng không chồng chéo. (Và cũng có thể giả sử căn chỉnh 16 byte trên một số triển khai x86-64, không phải là trường hợp cho định nghĩa độc lập.) Và nếu bạn biên dịch với gcc -O3 -march=native, bạn có thể nhận được 256-bit hoặc 512-bit vector hóa.
Peter Cordes

24

Điều đó có nghĩa là tôi không nên tin tưởng vào hiệu suất của ngôn ngữ lắp ráp được viết bởi bàn tay của tôi

Vâng, đó chính xác là những gì nó có nghĩa, và nó đúng với mọi ngôn ngữ. Nếu bạn không biết cách viết mã hiệu quả bằng ngôn ngữ X, thì bạn không nên tin tưởng vào khả năng viết mã hiệu quả của mình trong X. Và vì vậy, nếu bạn muốn mã hiệu quả, bạn nên sử dụng ngôn ngữ khác.

Hội đặc biệt nhạy cảm với điều này, bởi vì, tốt, những gì bạn thấy là những gì bạn nhận được. Bạn viết các hướng dẫn cụ thể mà bạn muốn CPU thực thi. Với các ngôn ngữ cấp cao, có một trình biên dịch trong betweeen, có thể chuyển đổi mã của bạn và loại bỏ nhiều sự thiếu hiệu quả. Với lắp ráp, bạn là của riêng bạn.


2
Tôi nghĩ rằng để viết rằng đặc biệt đối với bộ xử lý x86 hiện đại, đặc biệt khó viết mã lắp ráp hiệu quả do sự hiện diện của các đường ống, nhiều đơn vị thực thi và các mánh lới quảng cáo khác bên trong mỗi lõi. Viết mã cân bằng việc sử dụng tất cả các tài nguyên này để có được tốc độ thực thi cao nhất thường sẽ dẫn đến mã với logic không rõ ràng rằng "không nên" nhanh theo trí tuệ lắp ráp "thông thường". Nhưng đối với các CPU ít phức tạp hơn, đó là kinh nghiệm của tôi rằng việc tạo mã của trình biên dịch C có thể được cải thiện đáng kể.
Olof Forshell

4
Mã trình biên dịch C có thể được thường được bettered, ngay cả trên một CPU x86 hiện đại. Nhưng bạn phải hiểu rõ về CPU, điều này khó thực hiện hơn với CPU x86 hiện đại. Đó là quan điểm của tôi. Nếu bạn không hiểu phần cứng bạn đang nhắm mục tiêu, thì bạn sẽ không thể tối ưu hóa cho phần cứng. Và sau đó trình biên dịch có thể sẽ làm một công việc tốt hơn
jalf

1
Và nếu bạn thực sự muốn thổi bay trình biên dịch đi, bạn phải sáng tạo và tối ưu hóa theo cách mà trình biên dịch không thể. Đó là sự đánh đổi về thời gian / phần thưởng, đó là lý do tại sao C là ngôn ngữ kịch bản cho một số và mã trung gian cho ngôn ngữ cấp cao hơn cho những người khác. Đối với tôi mặc dù, lắp ráp là nhiều hơn cho niềm vui :). giống như grc.com/smgassinstall.htmlm
Hawken

22

Lý do duy nhất để sử dụng ngôn ngữ lắp ráp hiện nay là sử dụng một số tính năng mà ngôn ngữ không thể truy cập được.

Điều này áp dụng cho:

  • Lập trình kernel cần truy cập vào một số tính năng phần cứng nhất định như MMU
  • Lập trình hiệu suất cao sử dụng các hướng dẫn vectơ hoặc đa phương tiện rất cụ thể không được trình biên dịch của bạn hỗ trợ.

Nhưng các trình biên dịch hiện tại khá thông minh, chúng thậm chí có thể thay thế hai câu lệnh riêng biệt như d = a / b; r = a % b;bằng một lệnh duy nhất tính toán phép chia và phần còn lại trong một lần nếu nó khả dụng, ngay cả khi C không có toán tử đó.


10
Có những nơi khác cho ASM ngoài hai nơi đó. Cụ thể, một thư viện bignum thường sẽ nhanh hơn đáng kể trong ASM so với C, do có quyền truy cập để mang cờ và phần trên của phép nhân và như vậy. Bạn cũng có thể làm những điều này trong C xách tay, nhưng chúng rất chậm.
Vịt Mooing

@MooingDuck Điều đó có thể được coi là truy cập các tính năng phần cứng phần cứng không có sẵn trực tiếp bằng ngôn ngữ ... Nhưng miễn là bạn chỉ dịch mã cấp cao của mình sang lắp ráp bằng tay, trình biên dịch sẽ đánh bại bạn.
fortran

1
nó là vậy, nhưng nó không phải là lập trình kernel, cũng không phải là nhà cung cấp cụ thể. Mặc dù với những thay đổi nhỏ trong công việc, nó có thể dễ dàng rơi vào một trong hai loại. Id đoán ASM khi bạn muốn hiệu suất của các hướng dẫn bộ xử lý không có ánh xạ C.
Vịt Mooing

1
@fortran Về cơ bản bạn chỉ cần nói rằng nếu bạn không tối ưu hóa mã của mình thì nó sẽ không nhanh bằng mã trình biên dịch được tối ưu hóa. Tối ưu hóa là lý do người ta sẽ viết lắp ráp ở nơi đầu tiên. Nếu bạn có nghĩa là dịch thì tối ưu hóa thì không có lý do gì trình biên dịch sẽ đánh bại bạn trừ khi bạn không giỏi tối ưu hóa lắp ráp. Vì vậy, để đánh bại trình biên dịch, bạn phải tối ưu hóa theo cách mà trình biên dịch không thể. Nó khá tự giải thích. Lý do duy nhất để viết assembly là nếu bạn giỏi hơn trình biên dịch / trình thông dịch . Đó luôn là lý do thực tế để viết lắp ráp.
Hawken

1
Chỉ cần nói: Clang có quyền truy cập vào các cờ mang, nhân 128 bit, v.v. thông qua các hàm tích hợp. Và nó có thể tích hợp tất cả những điều này vào các thuật toán tối ưu hóa thông thường của nó.
gnasher729

19

Đúng là một trình biên dịch hiện đại thực hiện một công việc tuyệt vời để tối ưu hóa mã, nhưng tôi vẫn khuyến khích bạn tiếp tục học lắp ráp.

Trước hết, bạn rõ ràng không bị đe dọa bởi điều đó , đó là một điểm cộng tuyệt vời, tiếp theo - bạn đang đi đúng hướng bằng cách định hình để xác thực hoặc loại bỏ các giả định tốc độ của bạn , bạn đang yêu cầu đầu vào từ những người có kinh nghiệm và bạn có công cụ tối ưu hóa lớn nhất được nhân loại biết đến: một bộ não .

Khi kinh nghiệm của bạn tăng lên, bạn sẽ tìm hiểu khi nào và ở đâu để sử dụng nó (thường là các vòng lặp chặt chẽ nhất, trong cùng trong mã của bạn, sau khi bạn đã tối ưu hóa sâu sắc ở cấp độ thuật toán).

Để lấy cảm hứng, tôi khuyên bạn nên tra cứu các bài viết của Michael Abrash (nếu bạn chưa nghe gì về anh ta, anh ta là một bậc thầy tối ưu hóa, anh ta thậm chí còn hợp tác với John Carmack trong việc tối ưu hóa trình kết xuất phần mềm Quake!)

"không có thứ gọi là mã nhanh nhất" - Michael Abrash


2
Tôi tin rằng một trong những cuốn sách của Michael Abrash là sách đen lập trình đồ họa. Nhưng ông không phải là người duy nhất sử dụng lắp ráp, Chris Sawyer đã viết hai trò chơi trùm tàu ​​lượn siêu tốc đầu tiên do chính ông lắp ráp.
Hawken

14

Tôi đã thay đổi mã asm:

 __asm
{ 
    mov ebx,TIMES
 start:
    mov ecx,lengthOfArray
    mov esi,x
    shr ecx,2
    mov edi,y
label:
    mov eax,DWORD PTR [esi]
    add eax,DWORD PTR [edi]
    add edi,4   
    dec ecx 
    mov DWORD PTR [esi],eax
    add esi,4
    test ecx,ecx
    jnz label
    dec ebx
    test ebx,ebx
    jnz start
};

Kết quả cho phiên bản phát hành:

 Function of assembly version: 41
 Function of C++ version: 161

Mã lắp ráp trong chế độ phát hành nhanh hơn gần 4 lần so với C ++. IMHo, tốc độ của mã lắp ráp phụ thuộc vào Lập trình viên


Vâng, mã của tôi thực sự cần phải được tối ưu hóa. Làm việc tốt cho bạn và cảm ơn!
dùng957121

5
Nó nhanh hơn bốn lần vì bạn chỉ thực hiện một phần tư công việc :-) Điều shr ecx,2này là không cần thiết, bởi vì độ dài mảng đã được đưa vào intvà không tính bằng byte. Vì vậy, về cơ bản bạn đạt được tốc độ tương tự. Bạn có thể thử padddcâu trả lời từ harold, điều này sẽ thực sự nhanh hơn.
Gunther Piez

13

đó là một chủ đề rất thú vị
Tôi đã thay đổi MMX bằng SSE trong mã của Sasha
Đây là kết quả của tôi:

Function of C++ version:      315
Function of assembly(simply): 312
Function of assembly  (MMX):  136
Function of assembly  (SSE):  62

Mã lắp ráp với SSE nhanh hơn 5 lần so với C ++


12

Hầu hết các trình biên dịch ngôn ngữ cấp cao đều được tối ưu hóa và biết chúng đang làm gì. Bạn có thể thử và kết xuất mã tháo rời và so sánh nó với hội đồng gốc của bạn. Tôi tin rằng bạn sẽ thấy một số thủ thuật hay mà trình biên dịch của bạn đang sử dụng.

Ví dụ, thậm chí tôi không chắc nó đúng nữa :):

Đang làm:

mov eax,0

chi phí nhiều chu kỳ hơn

xor eax,eax

mà làm điều tương tự.

Trình biên dịch biết tất cả các thủ thuật này và sử dụng chúng.


4
Vẫn đúng, xem stackoverflow.com/questions/1396527/ . Không phải vì các chu kỳ được sử dụng, mà là do dung lượng bộ nhớ giảm.
Gunther Piez

10

Trình biên dịch đánh bại bạn. Tôi sẽ thử, nhưng tôi sẽ không đảm bảo. Tôi sẽ giả sử rằng "phép nhân" của TIMES có nghĩa là làm cho nó trở thành một bài kiểm tra hiệu suất phù hợp hơn, yxđược căn chỉnh 16 và đó lengthlà bội số khác không của 4. Dù sao thì điều đó có thể đúng.

  mov ecx,length
  lea esi,[y+4*ecx]
  lea edi,[x+4*ecx]
  neg ecx
loop:
  movdqa xmm0,[esi+4*ecx]
  paddd xmm0,[edi+4*ecx]
  movdqa [edi+4*ecx],xmm0
  add ecx,4
  jnz loop

Như tôi đã nói, tôi không đảm bảo. Nhưng tôi sẽ ngạc nhiên nếu nó có thể được thực hiện nhanh hơn nhiều - nút cổ chai ở đây là thông lượng bộ nhớ ngay cả khi mọi thứ đều là L1.


Tôi nghĩ rằng việc đánh địa chỉ phức tạp đang làm chậm mã của bạn, nếu bạn thay đổi mã thành mov ecx, length, lea ecx,[ecx*4], mov eax,16... add ecx,eaxvà sau đó chỉ cần sử dụng [esi + ecx] ở mọi nơi bạn sẽ tránh được chu kỳ 1 chu kỳ cho mỗi lệnh tăng tốc cho các vòng lặp. (Nếu bạn có Skylake mới nhất thì điều này không áp dụng). Việc thêm reg, reg chỉ làm cho vòng lặp chặt chẽ hơn, có thể có hoặc không có ích.
Johan

@Johan không nên là một gian hàng, chỉ là một độ trễ chu kỳ thêm, nhưng chắc chắn rằng nó không có hại gì khi không có nó .. Tôi đã viết mã này cho Core2 không có vấn đề đó. Không phải r + r cũng "phức tạp" btw sao?
harold

7

Chỉ cần mù quáng thực hiện cùng một thuật toán, hướng dẫn theo hướng dẫn, trong lắp ráp được đảm bảo chậm hơn so với những gì trình biên dịch có thể làm.

Đó là bởi vì ngay cả tối ưu hóa nhỏ nhất mà trình biên dịch thực hiện vẫn tốt hơn mã cứng nhắc của bạn mà không có tối ưu hóa nào cả.

Tất nhiên, có thể đánh bại trình biên dịch, đặc biệt nếu đó là một phần nhỏ, cục bộ của mã, tôi thậm chí phải tự làm điều đó để có được khoảng. Tăng tốc độ gấp 4 lần, nhưng trong trường hợp này chúng ta phải phụ thuộc rất nhiều vào kiến ​​thức tốt về phần cứng và nhiều thủ thuật dường như phản trực giác.


3
Tôi nghĩ rằng điều này phụ thuộc vào ngôn ngữ và trình biên dịch. Tôi có thể tưởng tượng một trình biên dịch C cực kỳ kém hiệu quả mà đầu ra của nó có thể dễ dàng bị đánh bại bởi một bộ phận viết đơn giản của con người. GCC, không quá nhiều.
Casey Rodarmor

Với trình biên dịch C / ++ là một công việc như vậy và chỉ có 3 trình biên dịch chính xung quanh, họ có xu hướng khá giỏi trong những gì họ làm. Vẫn có thể (rất) có thể trong một số trường hợp nhất định rằng lắp ráp viết tay sẽ nhanh hơn; rất nhiều thư viện toán học giảm xuống asm để xử lý tốt hơn nhiều giá trị / rộng. Vì vậy, trong khi đảm bảo là một chút quá mạnh mẽ, nó có khả năng.
ssube

@peachykeen: Tôi không có nghĩa là lắp ráp được đảm bảo chậm hơn C ++ nói chung. Tôi có nghĩa là "đảm bảo" trong trường hợp bạn có mã C ++ và dịch một cách mù quáng từng dòng để lắp ráp. Đọc đoạn cuối câu trả lời của tôi quá :)
vsz

5

Là một trình biên dịch, tôi sẽ thay thế một vòng lặp với kích thước cố định thành nhiều tác vụ thực thi.

int a = 10;
for (int i = 0; i < 3; i += 1) {
    a = a + i;
}

sẽ sản xuất

int a = 10;
a = a + 0;
a = a + 1;
a = a + 2;

và cuối cùng nó sẽ biết rằng "a = a + 0;" là vô dụng vì vậy nó sẽ loại bỏ dòng này. Hy vọng rằng một cái gì đó trong đầu của bạn bây giờ sẵn sàng để đính kèm một số tùy chọn tối ưu hóa như là một nhận xét. Tất cả những tối ưu hóa rất hiệu quả sẽ làm cho ngôn ngữ được biên dịch nhanh hơn.


4
Và trừ khi không aổn định, có khả năng trình biên dịch sẽ làm int a = 13;ngay từ đầu.
vsz

4

Đó chính xác là những gì nó có nghĩa. Để lại tối ưu hóa vi mô cho trình biên dịch.


4

Tôi thích ví dụ này vì nó thể hiện một bài học quan trọng về mã cấp thấp. Có, bạn có thể viết lắp ráp nhanh như mã C của bạn. Điều này đúng về mặt taut, nhưng không nhất thiết có nghĩa gì cả. Rõ ràng ai đó có thể, nếu không thì trình biên dịch sẽ không biết các tối ưu hóa phù hợp.

Tương tự như vậy, nguyên tắc tương tự được áp dụng khi bạn đi lên hệ thống phân cấp ngôn ngữ trừu tượng. Có, bạn có thể viết một trình phân tích cú pháp bằng C nhanh như một tập lệnh perl nhanh và bẩn, và nhiều người làm. Nhưng điều đó không có nghĩa là vì bạn đã sử dụng C, mã của bạn sẽ nhanh. Trong nhiều trường hợp, các ngôn ngữ cấp cao hơn thực hiện tối ưu hóa mà bạn có thể chưa bao giờ xem xét.


3

Trong nhiều trường hợp, cách tối ưu để thực hiện một số tác vụ có thể phụ thuộc vào bối cảnh mà tác vụ được thực hiện. Nếu một thói quen được viết bằng ngôn ngữ lắp ráp, thông thường sẽ không thể thay đổi chuỗi các hướng dẫn dựa trên ngữ cảnh. Ví dụ đơn giản, hãy xem xét phương pháp đơn giản sau:

inline void set_port_high(void)
{
  (*((volatile unsigned char*)0x40001204) = 0xFF);
}

Một trình biên dịch cho mã ARM 32 bit, được đưa ra ở trên, có thể sẽ hiển thị nó như một cái gì đó như:

ldr  r0,=0x40001204
mov  r1,#0
strb r1,[r0]
[a fourth word somewhere holding the constant 0x40001204]

hoặc có lẽ

ldr  r0,=0x40001000  ; Some assemblers like to round pointer loads to multiples of 4096
mov  r1,#0
strb r1,[r0+0x204]
[a fourth word somewhere holding the constant 0x40001000]

Điều đó có thể được tối ưu hóa một chút trong mã được lắp ráp bằng tay, như sau:

ldr  r0,=0x400011FF
strb r0,[r0+5]
[a third word somewhere holding the constant 0x400011FF]

hoặc là

mvn  r0,#0xC0       ; Load with 0x3FFFFFFF
add  r0,r0,#0x1200  ; Add 0x1200, yielding 0x400011FF
strb r0,[r0+5]

Cả hai cách tiếp cận được lắp ráp bằng tay sẽ cần 12 byte không gian mã thay vì 16; cái sau sẽ thay thế "tải" bằng "add", trên ARM7-TDMI thực hiện hai chu kỳ nhanh hơn. Nếu mã sẽ được thực thi trong bối cảnh r0 không biết / không quan tâm, thì các phiên bản ngôn ngữ lắp ráp sẽ có phần tốt hơn phiên bản được biên dịch. Mặt khác, giả sử trình biên dịch biết rằng một số thanh ghi [ví dụ r5] sẽ giữ một giá trị nằm trong 2047 byte của địa chỉ mong muốn 0x40001204 [ví dụ 0x40001000], và hơn nữa biết rằng một số thanh ghi khác [ví dụ r7] đang diễn ra để giữ một giá trị có bit thấp là 0xFF. Trong trường hợp đó, trình biên dịch có thể tối ưu hóa phiên bản C của mã thành đơn giản:

strb r7,[r5+0x204]

Ngắn hơn và nhanh hơn nhiều so với mã lắp ráp được tối ưu hóa bằng tay. Hơn nữa, giả sử set_port_high xảy ra trong bối cảnh:

int temp = function1();
set_port_high();
function2(temp); // Assume temp is not used after this

Hoàn toàn không hợp lý khi mã hóa cho một hệ thống nhúng. Nếu set_port_highđược viết bằng mã lắp ráp, trình biên dịch sẽ phải di chuyển r0 (giữ giá trị trả về từ function1một nơi khác trước khi gọi mã lắp ráp, sau đó di chuyển giá trị đó trở lại r0 sau đó (vì function2sẽ mong đợi tham số đầu tiên của nó trong r0), vì vậy mã lắp ráp "được tối ưu hóa" sẽ cần năm hướng dẫn. Ngay cả khi trình biên dịch không biết bất kỳ thanh ghi nào giữ địa chỉ hoặc giá trị cần lưu trữ, phiên bản bốn lệnh của nó (có thể điều chỉnh để sử dụng bất kỳ thanh ghi có sẵn nào - không nhất thiết là r0 và r1) sẽ đánh bại cụm "tối ưu hóa" phiên bản ngôn ngữ. Nếu trình biên dịch có địa chỉ và dữ liệu cần thiết trong r5 và r7 như được mô tả trước đó, function1sẽ không thay đổi các thanh ghi đó và do đó nó có thể thay thếset_port_highvới một strblệnh đơn - bốn lệnh nhỏ hơn và nhanh hơn mã lắp ráp "tối ưu hóa bằng tay".

Lưu ý rằng mã lắp ráp được tối ưu hóa bằng tay thường có thể vượt trội hơn trình biên dịch trong trường hợp lập trình viên biết luồng chương trình chính xác, nhưng trình biên dịch tỏa sáng trong trường hợp một đoạn mã được viết trước khi bối cảnh của nó được biết hoặc khi một đoạn mã nguồn có thể được gọi từ nhiều bối cảnh [nếu set_port_highđược sử dụng ở năm mươi vị trí khác nhau trong mã, trình biên dịch có thể quyết định độc lập cho từng cách tốt nhất để mở rộng nó].

Nói chung, tôi sẽ đề xuất rằng ngôn ngữ hợp ngữ có khả năng mang lại sự cải thiện hiệu suất lớn nhất trong những trường hợp mà mỗi đoạn mã có thể được tiếp cận từ một số ngữ cảnh rất hạn chế và có thể gây bất lợi cho hiệu suất ở những nơi có một phần mã có thể được tiếp cận từ nhiều bối cảnh khác nhau. Thật thú vị (và thuận tiện) các trường hợp lắp ráp có lợi nhất cho hiệu suất thường là những trường hợp mã đơn giản và dễ đọc nhất. Những nơi mà mã ngôn ngữ lắp ráp sẽ biến thành một mớ hỗn độn thường là những nơi mà việc viết trong lắp ráp sẽ mang lại lợi ích hiệu suất nhỏ nhất.

[Lưu ý nhỏ: có một số nơi có thể sử dụng mã lắp ráp để tạo ra một mớ hỗn độn siêu tối ưu hóa; ví dụ, một đoạn mã tôi đã làm cho ARM cần lấy một từ từ RAM và thực thi một trong khoảng mười hai thói quen dựa trên sáu bit trên của giá trị (nhiều giá trị được ánh xạ tới cùng một thói quen). Tôi nghĩ rằng tôi đã tối ưu hóa mã đó thành một cái gì đó như:

ldrh  r0,[r1],#2! ; Fetch with post-increment
ldrb  r1,[r8,r0 asr #10]
sub   pc,r8,r1,asl #2

Thanh ghi r8 luôn giữ địa chỉ của bảng công văn chính (trong vòng lặp mà mã dành 98% thời gian của nó, không có gì được sử dụng cho bất kỳ mục đích nào khác); tất cả 64 mục được đề cập đến địa chỉ trong 256 byte trước nó. Vì trong hầu hết các trường hợp, vòng lặp chính có giới hạn thời gian thực hiện cứng trong khoảng 60 chu kỳ, nên việc tìm nạp và gửi đi trong chu kỳ chín rất thuận lợi để đáp ứng mục tiêu đó. Sử dụng bảng 256 địa chỉ 32 bit sẽ nhanh hơn một chu kỳ, nhưng sẽ chiếm được 1KB RAM rất quý [flash sẽ có nhiều hơn một trạng thái chờ]. Sử dụng 64 địa chỉ 32 bit sẽ yêu cầu thêm một lệnh để che giấu một số bit từ từ đã tìm nạp và vẫn sẽ ngấu nghiến thêm 192 byte so với bảng tôi thực sự sử dụng. Sử dụng bảng bù 8 bit mang lại mã rất nhỏ gọn và nhanh chóng, nhưng không phải cái gì tôi mong đợi một trình biên dịch sẽ xuất hiện; Tôi cũng không mong đợi một trình biên dịch dành một đăng ký "toàn thời gian" để giữ địa chỉ bảng.

Đoạn mã trên được thiết kế để chạy như một hệ thống khép kín; nó có thể gọi mã C theo định kỳ, nhưng chỉ trong một số thời điểm nhất định khi phần cứng mà nó đang liên lạc có thể được đưa vào trạng thái "nhàn rỗi" một cách an toàn trong hai khoảng thời gian khoảng một phần nghìn giây mỗi 16ms.


2

Trong thời gian gần đây, tất cả các tối ưu hóa tốc độ mà tôi đã thực hiện là thay thế mã chậm bị hỏng não chỉ bằng mã hợp lý. Nhưng đối với mọi thứ là tốc độ thực sự quan trọng và tôi đã nỗ lực hết sức để tạo ra thứ gì đó nhanh chóng, kết quả luôn luôn là một quá trình lặp đi lặp lại, trong đó mỗi lần lặp lại đưa ra cái nhìn sâu sắc hơn về vấn đề, tìm cách giải quyết vấn đề với ít thao tác hơn. Tốc độ cuối cùng luôn phụ thuộc vào mức độ hiểu biết của tôi về vấn đề. Nếu ở bất kỳ giai đoạn nào tôi sử dụng mã lắp ráp hoặc mã C được tối ưu hóa quá mức, quá trình tìm giải pháp tốt hơn sẽ phải chịu đựng và kết quả cuối cùng sẽ chậm hơn.


2

C ++ nhanh hơn trừ khi bạn đang sử dụng ngôn ngữ lắp ráp với kiến ​​thức sâu hơn với cách chính xác.

Khi tôi viết mã trong ASM, tôi sắp xếp lại các hướng dẫn theo cách thủ công để CPU có thể thực thi song song nhiều hơn trong số chúng khi có thể. Tôi hầu như không sử dụng RAM khi tôi viết mã trong ASM chẳng hạn: Có thể có hơn 20000 dòng mã trong ASM và tôi chưa từng sử dụng Push / pop.

Bạn có khả năng có thể nhảy vào giữa opcode để tự sửa đổi mã và hành vi mà không bị phạt tự sửa đổi mã. Truy cập các thanh ghi mất 1 tick (đôi khi mất 0,25 tick) của CPU. Việc xử lý RAM có thể mất hàng trăm.

Đối với cuộc phiêu lưu ASM cuối cùng của tôi, tôi chưa bao giờ sử dụng RAM để lưu trữ một biến số (cho hàng ngàn dòng ASM). ASM có thể có khả năng nhanh hơn không tưởng so với C ++. Nhưng nó phụ thuộc vào rất nhiều yếu tố khác nhau, chẳng hạn như:

1. I was writing my apps to run on the bare metal.
2. I was writing my own boot loader that was starting my programs in ASM so there was no OS management in the middle.

Bây giờ tôi đang học C # và C ++ vì tôi nhận ra vấn đề năng suất !! Bạn có thể thử thực hiện các chương trình có thể tưởng tượng nhanh nhất bằng cách sử dụng ASM thuần túy trong thời gian rảnh. Nhưng để sản xuất một cái gì đó, sử dụng một số ngôn ngữ cấp cao.

Ví dụ, chương trình cuối cùng tôi đã mã hóa là sử dụng JS và GLSL và tôi không bao giờ nhận thấy bất kỳ vấn đề nào về hiệu năng, thậm chí nói về JS chậm. Điều này là do khái niệm đơn thuần về lập trình GPU cho 3D khiến tốc độ ngôn ngữ gửi các lệnh tới GPU gần như không liên quan.

Tốc độ của trình biên dịch một mình trên kim loại trần là không thể phủ nhận. Nó có thể chậm hơn trong C ++ không? - Có thể là do bạn đang viết mã lắp ráp với trình biên dịch không sử dụng trình biên dịch để bắt đầu.

Hội đồng cá nhân của tôi là không bao giờ viết mã lắp ráp nếu bạn có thể tránh nó, mặc dù tôi thích lắp ráp.


1

Tất cả các câu trả lời ở đây dường như loại trừ một khía cạnh: đôi khi chúng ta không viết mã để đạt được mục đích cụ thể, nhưng vì niềm vui tuyệt đối của nó. Có thể không kinh tế khi đầu tư thời gian để làm như vậy, nhưng có thể cho rằng không có sự thỏa mãn nào lớn hơn việc đánh bại đoạn mã được tối ưu hóa trình biên dịch nhanh nhất về tốc độ với một thay thế asm cuộn thủ công.


Khi bạn chỉ muốn đánh bại trình biên dịch, việc lấy đầu ra asm của nó cho chức năng của bạn thường dễ dàng hơn và biến nó thành một hàm asm độc lập mà bạn điều chỉnh. Sử dụng mã asm nội tuyến là một loạt các công việc bổ sung để có được giao diện giữa C ++ và asm chính xác và kiểm tra xem nó có đang biên dịch thành mã tối ưu hay không. (Nhưng ít nhất khi chỉ làm việc đó cho vui, bạn không phải lo lắng về việc nó đánh bại các tối ưu hóa như lan truyền liên tục khi hàm này chuyển sang một thứ khác. Gcc.gnu.org/wiki/DontUseInlineAsm ).
Peter Cordes

Xem thêm C ++ phỏng đoán C ++ so với hỏi đáp bằng văn bản để biết thêm về cách đánh bại trình biên dịch cho vui :) Và cũng đề xuất về cách sử dụng những gì bạn học để sửa đổi C ++ để giúp trình biên dịch tạo mã tốt hơn.
Peter Cordes

@PeterCordes Vì vậy, những gì bạn đang nói là bạn đồng ý.
madoki

1
Vâng, asm rất thú vị, ngoại trừ asm nội tuyến thường là lựa chọn sai ngay cả khi chơi xung quanh. Về mặt kỹ thuật đây là một câu hỏi nội tuyến, vì vậy sẽ tốt hơn nếu bạn giải quyết vấn đề này trong câu trả lời của bạn. Ngoài ra, đây thực sự là một nhận xét nhiều hơn là một câu trả lời.
Peter Cordes

OK đồng ý. Tôi đã từng là một chàng trai duy nhất nhưng đó là những năm 80.
madoki

-2

Một trình biên dịch c ++, sau khi tối ưu hóa ở cấp độ tổ chức, sẽ tạo ra mã sử dụng các chức năng tích hợp của cpu được nhắm mục tiêu. HLL sẽ không bao giờ vượt qua hoặc thực hiện trình biên dịch chương trình vì nhiều lý do; 1.) HLL sẽ được biên dịch và xuất ra với mã Accessor, kiểm tra ranh giới và có thể được xây dựng trong bộ sưu tập rác (trước đây là phạm vi địa chỉ trong phong cách OOP) tất cả các chu kỳ yêu cầu (flips và flops). HLL làm một công việc tuyệt vời ngày nay (bao gồm cả C ++ mới hơn và những người khác như GO), nhưng nếu chúng vượt trội hơn trình biên dịch mã (cụ thể là mã của bạn), bạn cần tham khảo tài liệu CPU-so sánh với mã cẩu thả chắc chắn là không thể kết luận và các trình biên dịch như trình biên dịch xuống mã op HLL trừu tượng hóa các chi tiết và không loại bỏ chúng nếu ứng dụng của bạn sẽ không chạy nếu nó thậm chí còn được hệ điều hành máy chủ nhận ra.

Hầu hết mã trình biên dịch mã (chủ yếu là các đối tượng) là đầu ra "không đầu" để đưa vào các định dạng thực thi khác với yêu cầu xử lý ít hơn rất nhiều do đó sẽ nhanh hơn nhiều, nhưng không an toàn hơn nhiều; nếu một trình thực thi được đầu ra bởi trình biên dịch chương trình (NAsm, YAsm; v.v.) thì nó vẫn sẽ chạy nhanh hơn cho đến khi nó hoàn toàn khớp với mã HLL trong chức năng thì kết quả có thể được cân nhắc chính xác.

Việc gọi một đối tượng mã dựa trên trình biên dịch mã từ HLL theo bất kỳ định dạng nào vốn sẽ thêm chi phí xử lý ngoài các cuộc gọi không gian bộ nhớ sử dụng bộ nhớ được cấp phát toàn cầu cho các loại dữ liệu biến / không đổi (điều này áp dụng cho cả LLL và HLL). Hãy nhớ rằng đầu ra cuối cùng đang sử dụng CPU cuối cùng là api và abi của nó so với phần cứng (opcode) và cả hai, trình biên dịch và "trình biên dịch HLL" về cơ bản là giống hệt nhau với ngoại lệ thực sự duy nhất là khả năng đọc (ngữ pháp).

Ứng dụng bảng điều khiển Hello thế giới trong trình biên dịch chương trình sử dụng FAsm là 1,5 KB (và điều này là trong Windows thậm chí còn nhỏ hơn trong FreeBSD và Linux) và vượt trội hơn bất cứ thứ gì GCC có thể ném vào ngày tốt nhất của nó; lý do là đệm ẩn với nops, xác thực truy cập và kiểm tra ranh giới để đặt tên cho một số. Mục tiêu thực sự là các lib HLL sạch và một trình biên dịch tối ưu hóa nhắm mục tiêu một cpu theo cách "khó tính" và hầu hết làm những ngày này (cuối cùng). GCC không tốt hơn YAsm - đó là cách thực hành và hiểu biết về mã hóa của nhà phát triển đang được đề cập và "tối ưu hóa" xuất hiện sau khi khám phá và đào tạo & kinh nghiệm tạm thời.

Trình biên dịch phải liên kết và lắp ráp cho đầu ra trong cùng một opcode như một trình biên dịch vì các mã đó là tất cả những gì CPU sẽ ngoại trừ (CISC hoặc RISC [PIC quá]). YAsm đã tối ưu hóa và dọn dẹp rất nhiều cho NAsm sớm, cuối cùng tăng tốc tất cả đầu ra từ trình biên dịch đó, nhưng ngay cả khi đó YAsm vẫn, như NAsm, tạo ra các tệp thực thi với các phụ thuộc bên ngoài nhắm vào các thư viện hệ điều hành thay mặt cho nhà phát triển để số dặm có thể thay đổi. Kết thúc C ++ là một điểm đáng kinh ngạc và an toàn hơn nhiều so với trình biên dịch mã cho hơn 80%, đặc biệt là trong lĩnh vực thương mại ...


1
C và C ++ không có bất kỳ giới hạn nào - kiểm tra trừ khi bạn yêu cầu và không có bộ sưu tập rác trừ khi bạn tự thực hiện hoặc sử dụng thư viện. Câu hỏi thực sự là liệu trình biên dịch có tạo ra các vòng lặp (và tối ưu hóa toàn cầu) tốt hơn so với con người hay không. Thông thường là có, trừ khi con người thực sự biết họ đang làm gì và dành nhiều thời gian cho nó .
Peter Cordes

1
Bạn có thể tạo các tệp thực thi tĩnh bằng cách sử dụng NASM hoặc YASM (không có mã bên ngoài). Cả hai đều có thể xuất ra ở định dạng nhị phân phẳng, vì vậy bạn có thể tự mình lắp ráp các tiêu đề ELF nếu bạn thực sự không muốn chạy ld, nhưng điều đó không có gì khác biệt trừ khi bạn đang cố gắng tối ưu hóa kích thước tệp (không chỉ kích thước của đoạn văn bản). Xem Hướng dẫn về cơn lốc trong việc tạo các tệp thực thi ELF thực sự dành cho Linux .
Peter Cordes

1
Có lẽ bạn đang nghĩ về C # hoặc std::vectorđược biên dịch trong chế độ gỡ lỗi. Mảng C ++ không như thế. Trình biên dịch có thể kiểm tra công cụ tại thời gian biên dịch, nhưng trừ khi bạn kích hoạt các tùy chọn làm cứng thêm, không có kiểm tra thời gian chạy. Xem ví dụ một hàm làm tăng 1024 phần tử đầu tiên của một đối số int array[]. Đầu ra asm không có kiểm tra thời gian chạy: godbolt.org/g/w1HF5t . Tất cả những gì nó nhận được là một con trỏ trong rdi, không có thông tin kích thước. Lập trình viên phải tránh hành vi không xác định bằng cách không bao giờ gọi nó với một mảng nhỏ hơn 1024.
Peter Cordes

1
Bất cứ điều gì bạn đang nói không phải là một mảng C ++ đơn giản (phân bổ new, xóa thủ công delete, không kiểm tra giới hạn). Bạn có thể sử dụng C ++ để tạo mã asm / mã máy cồng kềnh (giống như hầu hết các phần mềm), nhưng đó là lỗi của lập trình viên, không phải của C ++. Bạn thậm chí có thể sử dụng allocađể phân bổ không gian ngăn xếp dưới dạng một mảng.
Peter Cordes

1
Liên kết một ví dụ trên gcc.godbolt.org để g++ -O3tạo giới hạn - kiểm tra mã cho một mảng đơn giản hoặc làm bất cứ điều gì khác mà bạn đang nói về. C ++ làm cho nó dễ dàng hơn nhiều để tạo ra mã nhị phân cồng kềnh (và trong thực tế, bạn phải cẩn thận không để nếu bạn đang nhắm đến hiệu suất), nhưng nó không phải theo nghĩa đen không thể tránh khỏi. Nếu bạn hiểu cách C ++ biên dịch thành asm, bạn có thể nhận được mã chỉ kém hơn bạn có thể viết bằng tay, nhưng với nội tuyến và lan truyền liên tục trên quy mô lớn hơn bạn có thể quản lý bằng tay.
Peter Cordes

-3

Hội có thể nhanh hơn nếu trình biên dịch của bạn tạo ra nhiều mã hỗ trợ OO .

Biên tập:

Để downvoters: OP đã viết "tôi có nên ... tập trung vào C ++ và quên ngôn ngữ lắp ráp không?" và tôi đứng trước câu trả lời của tôi. Bạn luôn cần để mắt đến mã OO tạo ra, đặc biệt là khi sử dụng các phương thức. Không quên về ngôn ngữ lắp ráp có nghĩa là bạn sẽ định kỳ xem lại quá trình lắp ráp mà mã OO của bạn tạo ra mà tôi tin là bắt buộc để viết phần mềm hoạt động tốt.

Trên thực tế, điều này liên quan đến tất cả các mã có thể biên dịch, không chỉ OO.


2
-1: Tôi không thấy bất kỳ tính năng OO nào đang được sử dụng. Đối số của bạn giống như "lắp ráp cũng có thể nhanh hơn nếu trình biên dịch của bạn thêm một triệu NOP."
Sjoerd

Tôi đã không rõ ràng, đây thực sự là một câu hỏi C. Nếu bạn viết mã C cho trình biên dịch C ++, bạn sẽ không viết mã C ++ và bạn sẽ không nhận được bất kỳ nội dung OO nào. Khi bạn bắt đầu viết bằng C ++ thực, sử dụng công cụ OO, bạn phải rất am hiểu để có được trình biên dịch để không tạo mã hỗ trợ OO.
Olof Forshell

Vì vậy, câu trả lời của bạn không phải là về câu hỏi? (Ngoài ra, làm rõ trong câu trả lời, không phải bình luận. Nhận xét có thể bị xóa bất cứ lúc nào mà không cần thông báo, thông báo hoặc lịch sử.
Mooing Duck

1
Không chắc chắn chính xác ý bạn là gì bởi "mã hỗ trợ" của OO. Tất nhiên, nếu bạn sử dụng nhiều RTTI và tương tự, trình biên dịch sẽ phải tạo ra nhiều hướng dẫn bổ sung để hỗ trợ các tính năng đó - nhưng bất kỳ vấn đề nào đủ mức độ cao để phê chuẩn việc sử dụng RTTI đều quá phức tạp để có thể ghi được trong quá trình lắp ráp . Tất nhiên, những gì bạn có thể làm là chỉ viết giao diện bên ngoài trừu tượng dưới dạng OO, gửi đến mã thủ tục thuần túy được tối ưu hóa hiệu suất trong trường hợp quan trọng. Nhưng, tùy thuộc vào ứng dụng, C, Fortran, CUDA hoặc đơn giản là C ++ mà không có kế thừa ảo có thể tốt hơn lắp ráp tại đây.
leftaroundabout

2
Không. Ít nhất là không có khả năng lắm. Có một điều trong C ++ được gọi là quy tắc không chi phí và điều này được áp dụng hầu hết thời gian. Tìm hiểu thêm về OO - bạn sẽ thấy rằng cuối cùng, nó cải thiện khả năng đọc mã của bạn, cải thiện chất lượng mã, tăng tốc độ mã hóa, tăng độ mạnh mẽ. Cũng để nhúng - nhưng sử dụng C ++ vì nó mang lại cho bạn nhiều quyền kiểm soát hơn, nhúng + OO theo cách Java sẽ khiến bạn phải trả giá.
Zane
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.