Tại sao trình biên dịch không thể (hoặc không) tối ưu hóa vòng lặp bổ sung có thể dự đoán thành phép nhân?


133

Đây là một câu hỏi xuất hiện trong khi đọc câu trả lời xuất sắc của Mysticial cho câu hỏi: tại sao xử lý một mảng được sắp xếp nhanh hơn một mảng chưa sắp xếp ?

Bối cảnh cho các loại liên quan:

const unsigned arraySize = 32768;
int data[arraySize];
long long sum = 0;

Trong câu trả lời của mình, anh giải thích rằng Trình biên dịch Intel (ICC) tối ưu hóa điều này:

for (int i = 0; i < 100000; ++i)
    for (int c = 0; c < arraySize; ++c)
        if (data[c] >= 128)
            sum += data[c];

... thành một cái gì đó tương đương với điều này:

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

Trình tối ưu hóa nhận ra rằng những cái này là tương đương và do đó trao đổi các vòng lặp , di chuyển nhánh bên ngoài vòng lặp bên trong. Rất thông minh!

Nhưng tại sao nó không làm điều này?

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000 * data[c];

Hy vọng rằng Mysticial (hoặc bất cứ ai khác) có thể đưa ra một câu trả lời xuất sắc không kém. Tôi chưa bao giờ tìm hiểu về các tối ưu hóa được thảo luận trong câu hỏi khác trước đây, vì vậy tôi thực sự biết ơn về điều này.


14
Đó là điều mà có lẽ chỉ Intel mới biết. Tôi không biết thứ tự nào nó chạy tối ưu hóa. Và rõ ràng, nó không chạy vượt qua vòng lặp sau khi trao đổi vòng lặp.
Bí ẩn

7
Tối ưu hóa này chỉ hợp lệ nếu các giá trị trong mảng dữ liệu là bất biến. Chẳng hạn, nếu bộ nhớ được ánh xạ tới thiết bị đầu vào / đầu ra mỗi lần bạn đọc dữ liệu [0] sẽ tạo ra một giá trị khác ...
Thomas CG de Vilhena

2
Kiểu dữ liệu này là số nguyên hay dấu phẩy động? Phép cộng lặp lại trong dấu phẩy động cho kết quả rất khác so với phép nhân.
Ben Voigt

6
@Thomas: Nếu dữ liệu là volatile, thì trao đổi vòng lặp cũng sẽ là một tối ưu hóa không hợp lệ.
Ben Voigt

3
GNAT (trình biên dịch Ada với GCC 4.6) sẽ không chuyển đổi các vòng lặp ở O3, nhưng nếu các vòng lặp được chuyển đổi, nó sẽ chuyển đổi nó thành một phép nhân.
prosfilaes

Câu trả lời:


105

Trình biên dịch thường không thể chuyển đổi

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

vào

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000 * data[c];

bởi vì cái sau có thể dẫn đến tràn số nguyên đã ký mà cái trước không. Ngay cả với hành vi bao quanh được bảo đảm cho tràn số nguyên bổ sung đã ký hai, nó sẽ thay đổi kết quả (nếu data[c]là 30000, sản phẩm sẽ trở thành s -129496729632-bit điển hình intcó bao quanh, trong khi 100000 lần thêm 30000 sumsẽ, không tràn, tăng thêm sum3000000000). Lưu ý rằng cùng một số lượng không dấu, với các số khác nhau, tràn 100000 * data[c]thường sẽ đưa ra một modulo giảm 2^32không xuất hiện trong kết quả cuối cùng.

Nó có thể biến nó thành

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000LL * data[c];  // resp. 100000ull

mặc dù, nếu, như thường lệ, long longlà đủ lớn hơn int.

Tại sao nó không làm điều đó, tôi không thể nói, tôi đoán đó là những gì Mysticial đã nói , "rõ ràng, nó không chạy một đường chuyền sụp đổ sau khi trao đổi vòng lặp".

Lưu ý rằng bản thân trao đổi vòng lặp thường không hợp lệ (đối với số nguyên đã ký), vì

for (int c = 0; c < arraySize; ++c)
    if (condition(data[c]))
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

có thể dẫn đến tràn

for (int i = 0; i < 100000; ++i)
    for (int c = 0; c < arraySize; ++c)
        if (condition(data[c]))
            sum += data[c];

sẽ không. Ở đây sẽ tốt hơn, vì điều kiện đảm bảo tất cả những data[c]gì được thêm vào đều có cùng một dấu hiệu, vì vậy nếu một cái tràn ra, cả hai đều làm.

Tuy nhiên, tôi không chắc chắn rằng trình biên dịch đã tính đến điều đó (@Mysticial, bạn có thể thử với một điều kiện như thế data[c] & 0x80hoặc có thể đúng với các giá trị dương và âm không?). Tôi đã có các trình biên dịch thực hiện tối ưu hóa không hợp lệ (ví dụ, một vài năm trước, tôi đã sử dụng ICC (11.0, iirc) sử dụng chuyển đổi có chữ ký 32-bit-int-to-double trong 1.0/nđó nlà một unsigned inttốc độ nhanh gấp hai lần gcc đầu ra. Nhưng sai, rất nhiều giá trị lớn hơn 2^31, rất tiếc.).


4
Tôi nhớ một phiên bản của trình biên dịch MPW đã thêm một tùy chọn để cho phép các khung stack lớn hơn 32K [các phiên bản trước đó bị giới hạn khi sử dụng địa chỉ @ A7 + int16 cho các biến cục bộ]. Nó có mọi thứ phù hợp với các khung stack dưới 32K hoặc hơn 64K, nhưng đối với khung stack 40K thì nó sẽ sử dụng ADD.W A6,$A000, quên rằng các thao tác từ với các thanh ghi địa chỉ đăng nhập mở rộng từ thành 32 bit trước khi thêm. Mất một lúc để khắc phục sự cố, vì điều duy nhất mà mã đã làm giữa điều đó ADDvà lần tiếp theo nó bật ra khỏi ngăn xếp là để khôi phục các thanh ghi của người gọi, nó đã lưu vào khung đó ...
supercat

3
... và đăng ký duy nhất mà người gọi tình cờ quan tâm là địa chỉ [hằng số thời gian tải] của một mảng tĩnh. Trình biên dịch biết rằng địa chỉ của mảng đã được lưu trong một thanh ghi để nó có thể tối ưu hóa dựa trên điều đó, nhưng trình gỡ lỗi chỉ đơn giản biết địa chỉ của hằng. Do đó, trước một tuyên bố MyArray[0] = 4;tôi có thể kiểm tra phần bổ sung MyArrayvà xem vị trí đó trước và sau khi câu lệnh được thực thi; nó sẽ không thay đổi Mã là một cái gì đó giống như move.B @A3,#4và A3 được cho là luôn luôn chỉ ra MyArraybất cứ lúc nào lệnh được thực thi, nhưng nó đã không. Vui vẻ.
supercat

Vậy thì tại sao clang thực hiện loại tối ưu hóa này?
Jason S

Trình biên dịch có thể thực hiện việc viết lại trong các biểu diễn trung gian bên trong của nó, bởi vì nó được phép có ít hành vi không xác định trong các biểu diễn trung gian bên trong của nó.
dùng253751

48

Câu trả lời này không áp dụng cho trường hợp cụ thể được liên kết, nhưng nó áp dụng cho tiêu đề câu hỏi và có thể thú vị cho độc giả tương lai:

Do độ chính xác hữu hạn, phép cộng dấu phẩy động lặp lại không tương đương với phép nhân . Xem xét:

float const step = 1e-15;
float const init = 1;
long int const count = 1000000000;

float result1 = init;
for( int i = 0; i < count; ++i ) result1 += step;

float result2 = init;
result2 += step * count;

cout << (result1 - result2);

Bản giới thiệu


10
Đây không phải là câu trả lời cho câu hỏi. Mặc dù có thông tin thú vị (và phải biết đối với bất kỳ lập trình viên C / C ++ nào), đây không phải là diễn đàn và không thuộc về nơi này.
orlp

30
@nightcracker: Mục tiêu đã nêu của StackOverflow là xây dựng thư viện câu trả lời có thể tìm kiếm hữu ích cho người dùng trong tương lai. Và đây là một câu trả lời cho câu hỏi được hỏi ... thực tế là có một số thông tin không có căn cứ khiến câu trả lời này không áp dụng cho poster gốc. Nó vẫn có thể áp dụng cho những người khác có cùng câu hỏi.
Ben Voigt

12
có thể là một câu trả lời cho tiêu đề câu hỏi , nhưng không phải là câu hỏi, không.
orlp

7
Như tôi đã nói, đó là thông tin thú vị . Tuy nhiên, điều đó vẫn có vẻ sai đối với tôi rằng hiện tại, câu trả lời hàng đầu của câu hỏi không trả lời câu hỏi như hiện tại . Đây đơn giản không phải là lý do Intel Compiler quyết định không tối ưu hóa, basta.
orlp

4
@nightcracker: Có vẻ như tôi cũng sai vì đây là câu trả lời hàng đầu. Tôi hy vọng ai đó đăng một câu trả lời thực sự tốt cho trường hợp số nguyên vượt qua điểm này. Thật không may, tôi không nghĩ rằng có một câu trả lời cho "không thể" cho trường hợp số nguyên, bởi vì việc chuyển đổi sẽ là hợp pháp, vì vậy chúng tôi để lại "tại sao nó không", thực sự rơi vào tình trạng " quá cục bộ "lý do gần gũi, bởi vì nó đặc biệt với một phiên bản trình biên dịch cụ thể. Câu hỏi tôi trả lời là câu hỏi quan trọng hơn, IMO.
Ben Voigt

6

Trình biên dịch chứa nhiều lượt khác nhau để tối ưu hóa. Thông thường trong mỗi lần vượt qua, tối ưu hóa trên các câu lệnh hoặc tối ưu hóa vòng lặp được thực hiện. Hiện tại không có mô hình nào tối ưu hóa thân vòng lặp dựa trên các tiêu đề vòng lặp. Điều này là khó phát hiện và ít phổ biến hơn.

Việc tối ưu hóa được thực hiện là chuyển động mã bất biến vòng lặp. Điều này có thể được thực hiện bằng cách sử dụng một bộ các kỹ thuật.


4

Chà, tôi đoán rằng một số trình biên dịch có thể thực hiện loại tối ưu hóa này, giả sử rằng chúng ta đang nói về Integer Arithologists.

Đồng thời, một số trình biên dịch có thể từ chối thực hiện vì thay thế phép cộng lặp lại bằng phép nhân có thể thay đổi hành vi tràn của mã. Đối với các loại số nguyên không dấu, không nên tạo sự khác biệt vì hành vi tràn của chúng được chỉ định hoàn toàn bởi ngôn ngữ. Nhưng đối với những người đã ký, nó có thể (có lẽ không phải trên nền tảng bổ sung của 2). Đúng là tràn tràn đã ký thực sự dẫn đến hành vi không xác định trong C, có nghĩa là hoàn toàn ổn khi bỏ qua ngữ nghĩa tràn đó hoàn toàn, nhưng không phải tất cả các trình biên dịch đều đủ can đảm để làm điều đó. Nó thường thu hút rất nhiều lời chỉ trích từ đám đông "C chỉ là một ngôn ngữ lắp ráp cấp cao hơn". (Hãy nhớ những gì đã xảy ra khi GCC giới thiệu tối ưu hóa dựa trên ngữ nghĩa răng cưa nghiêm ngặt?)

Trong lịch sử, GCC đã thể hiện mình là một trình biên dịch có những bước cần thiết để thực hiện các bước quyết liệt như vậy, nhưng các trình biên dịch khác có thể thích gắn bó với hành vi "hướng đến người dùng" được nhận thức ngay cả khi ngôn ngữ không được xác định.


Tôi muốn biết liệu tôi có vô tình phụ thuộc vào hành vi không xác định hay không, nhưng tôi đoán trình biên dịch không có cách nào để biết vì tràn sẽ là vấn đề thời gian chạy: /
jhabbott

2
@jhabbott: iff xảy ra tràn, sau đó có hành vi không xác định. Cho dù hành vi được xác định là không xác định cho đến khi thời gian chạy (giả sử các số được nhập vào thời gian chạy): P.
orlp

3

Nó hiện tại - ít nhất, tiếng kêu vang :

long long add_100k_signed(int *data, int arraySize)
{
    long long sum = 0;

    for (int c = 0; c < arraySize; ++c)
        if (data[c] >= 128)
            for (int i = 0; i < 100000; ++i)
                sum += data[c];
    return sum;
}

biên dịch với -O1 thành

add_100k_signed:                        # @add_100k_signed
        test    esi, esi
        jle     .LBB0_1
        mov     r9d, esi
        xor     r8d, r8d
        xor     esi, esi
        xor     eax, eax
.LBB0_4:                                # =>This Inner Loop Header: Depth=1
        movsxd  rdx, dword ptr [rdi + 4*rsi]
        imul    rcx, rdx, 100000
        cmp     rdx, 127
        cmovle  rcx, r8
        add     rax, rcx
        add     rsi, 1
        cmp     r9, rsi
        jne     .LBB0_4
        ret
.LBB0_1:
        xor     eax, eax
        ret

Tràn số nguyên không có gì để làm với nó; nếu có tràn số nguyên gây ra hành vi không xác định, nó có thể xảy ra trong cả hai trường hợp. Đây là loại chức năng tương tự sử dụng intthay vìlong :

int add_100k_signed(int *data, int arraySize)
{
    int sum = 0;

    for (int c = 0; c < arraySize; ++c)
        if (data[c] >= 128)
            for (int i = 0; i < 100000; ++i)
                sum += data[c];
    return sum;
}

biên dịch với -O1 thành

add_100k_signed:                        # @add_100k_signed
        test    esi, esi
        jle     .LBB0_1
        mov     r9d, esi
        xor     r8d, r8d
        xor     esi, esi
        xor     eax, eax
.LBB0_4:                                # =>This Inner Loop Header: Depth=1
        mov     edx, dword ptr [rdi + 4*rsi]
        imul    ecx, edx, 100000
        cmp     edx, 127
        cmovle  ecx, r8d
        add     eax, ecx
        add     rsi, 1
        cmp     r9, rsi
        jne     .LBB0_4
        ret
.LBB0_1:
        xor     eax, eax
        ret

2

Có một rào cản về khái niệm cho loại tối ưu hóa này. Các tác giả trình biên dịch dành rất nhiều nỗ lực để giảm sức mạnh - ví dụ, thay thế phép nhân bằng phép cộng và dịch chuyển. Họ quen với việc nghĩ rằng bội số là xấu. Vì vậy, một trường hợp mà người ta phải đi theo con đường khác là đáng ngạc nhiên và phản trực giác. Vì vậy, không ai nghĩ để thực hiện nó.


3
Thay thế một vòng lặp bằng phép tính dạng đóng cũng là giảm cường độ, phải không?
Ben Voigt

Chính thức, vâng, tôi cho rằng, nhưng tôi chưa bao giờ nghe ai nói về nó theo cách đó. (Tuy nhiên, tôi hơi lạc hậu về tài liệu.)
zwol

1

Những người phát triển và duy trì trình biên dịch có một lượng thời gian và năng lượng hạn chế để dành cho công việc của họ, vì vậy họ thường muốn tập trung vào những gì người dùng của họ quan tâm nhất: biến mã được viết tốt thành mã nhanh. Họ không muốn dành thời gian cố gắng tìm cách biến mã ngớ ngẩn thành mã nhanh, đó là những gì mã đánh giá dành cho. Trong một ngôn ngữ cấp cao, có thể có mã "ngớ ngẩn" thể hiện một ý tưởng quan trọng, làm cho nó đáng để các nhà phát triển tạo ra ví dụ nhanh đó, phá rừng ngắn và hợp nhất luồng cho phép các chương trình Haskell được cấu trúc xung quanh một số loại lười biếng cấu trúc dữ liệu được tạo ra sẽ được biên dịch thành các vòng lặp chặt chẽ không phân bổ bộ nhớ. Nhưng loại khuyến khích đó đơn giản là không áp dụng để biến phép cộng lặp thành phép nhân. Nếu bạn muốn nó được nhanh chóng,

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.