Thay đổi lớp phức tạp thông qua tối ưu hóa trình biên dịch?


8

Tôi đang tìm kiếm một ví dụ trong đó một thuật toán rõ ràng đang thay đổi lớp phức tạp của nó do các chiến lược tối ưu hóa trình biên dịch và / hoặc bộ xử lý.


2
Điều này chắc chắn có thể xảy ra đối với sự phức tạp của không gian với một cái gì đó như loại bỏ đệ quy đuôi.
Bóng Eliot

4
Đơn giản: một vòng lặp trống O (n) có thể được tối ưu hóa đi O (1): xem bài đăng SO này: stackoverflow.com/questions/10300253/ Kẻ
Doc Brown

Việc tìm các ví dụ về điều này trong Haskell dễ dàng hơn, mặc dù nó không thực sự tối ưu hóa - chỉ là ngữ nghĩa lười biếng của ngôn ngữ có nghĩa là các đoạn mã lớn có khả năng cho các hàm được gọi là sẽ không được đánh giá vì kết quả không bao giờ được sử dụng. Nó thậm chí còn khá phổ biến trong Haskell để xác định các hàm đệ quy không giới hạn trả về danh sách vô hạn. Miễn là bạn chỉ sử dụng một đoạn hữu hạn của danh sách, chỉ có một lượng đệ quy hữu hạn được đánh giá (đủ kỳ lạ, có thể không đệ quy) và chỉ phần hữu hạn của danh sách được tính.
Steve314

1
@ Steve314: Tôi tin rằng có một ví dụ về điều này trong Trò chơi Điểm chuẩn Ngôn ngữ Máy tính, trong đó việc triển khai Haskell nhanh hơn 10 lần so với triển khai C do thực tế đơn giản là kết quả của điểm chuẩn không bao giờ được in và do đó toàn bộ chương trình Haskell biên dịch xuốngint main(void) { exit(0); };
Jörg W Mittag

@ Jörg - nó sẽ không làm tôi ngạc nhiên, nhưng tôi nghĩ rằng các nhà phát triển Haskell đã gian lận. Bạn có thể buộc một cái gì đó được đánh giá một cách háo hức trong Haskell nếu bạn cần và thật kỳ lạ, nó chủ yếu ở đó để tối ưu hóa - đánh giá nghiêm ngặt / háo hức thường nhanh hơn nhiều so với lười biếng vì nó tránh được các chi phí để đánh giá chậm. Một trình biên dịch Haskell tốt nắm bắt được rất nhiều điều này bằng cách sử dụng "phân tích nghiêm ngặt", nhưng có những lúc bạn phải buộc vấn đề.
Steve314

Câu trả lời:


4

Hãy thực hiện một chương trình đơn giản in hình vuông của một số được nhập trên dòng lệnh.

#include <stdio.h>

int main(int argc, char **argv) {
    int num = atoi(argv[1]);
    printf("%d\n",num);
    int i = 0;
    int total = 0;
    for(i = 0; i < num; i++) {
        total += num;
    }
    printf("%d\n",total);
    return 0;
}

Như bạn có thể thấy, đây là phép tính O (n), lặp đi lặp lại nhiều lần.

Biên dịch cái này với gcc -Smột phân đoạn là:

LBB1_1:
        movl    -36(%rbp), %eax
        movl    -28(%rbp), %ecx
        addl    %ecx, %eax
        movl    %eax, -36(%rbp)
        movl    -32(%rbp), %eax
        addl    $1, %eax
        movl    %eax, -32(%rbp)
LBB1_2:
        movl    -32(%rbp), %eax
        movl    -28(%rbp), %ecx
        cmpl    %ecx, %eax
        jl      LBB1_1

Trong phần này, bạn có thể thấy phần bổ sung được thực hiện, so sánh và nhảy lại cho vòng lặp.

Thực hiện biên dịch với gcc -S -O3để tối ưu hóa phân đoạn giữa các lệnh gọi tới printf:

        callq   _printf
        testl   %ebx, %ebx
        jg      LBB1_2
        xorl    %ebx, %ebx
        jmp     LBB1_3
LBB1_2:
        imull   %ebx, %ebx
LBB1_3:
        movl    %ebx, %esi
        leaq    L_.str(%rip), %rdi
        xorb    %al, %al
        callq   _printf

Bây giờ người ta có thể thấy thay vì nó không có vòng lặp và hơn nữa, không có thêm. Thay vào đó, có một cuộc gọi imullnhân số đó với chính nó.

Trình biên dịch đã nhận ra một vòng lặp và toán tử toán học bên trong và thay thế nó bằng phép tính thích hợp.


Lưu ý rằng điều này bao gồm một cuộc gọi atoiđể lấy số. Khi số đã tồn tại trong mã, trình biên dịch sẽ tính toán trước giá trị thay vì thực hiện các cuộc gọi thực tế như được so sánh giữa hiệu suất của sqrt trong C # và C trong đó sqrt(2)(một hằng số) được tính tổng trên một vòng lặp 1.000.000 lần.


7

Tối ưu hóa cuộc gọi đuôi có thể làm giảm độ phức tạp không gian. Ví dụ, không có TCO, việc thực hiện đệ quy whilevòng lặp này có độ phức tạp không gian trong trường hợp xấu nhất Ο(#iterations), trong khi với TCO, nó có độ phức tạp không gian trong trường hợp xấu nhất là Ο(1):

// This is Scala, but it works the same way in every other language.
def loop(cond: => Boolean)(body: => Unit): Unit = if (cond) { body; loop(cond)(body) }

var i = 0
loop { i < 3 } { i += 1; println(i) }
// 1
// 2
// 3

// E.g. ECMAScript:
function loop(cond, body) {
    if (cond()) { body(); loop(cond, body); };
};

var i = 0;
loop(function { return i < 3; }, function { i++; print(i); });

Điều này thậm chí không cần TCO chung, nó chỉ cần một trường hợp đặc biệt rất hẹp, cụ thể là loại bỏ đệ quy đuôi trực tiếp.

Tuy nhiên, điều sẽ rất thú vị là khi tối ưu hóa trình biên dịch không chỉ thay đổi lớp phức tạp mà thực sự thay đổi hoàn toàn thuật toán.

Trình biên dịch Haskell của Glorious đôi khi thực hiện điều này, nhưng đó không thực sự là điều tôi đang nói, nó giống như gian lận hơn. GHC có Ngôn ngữ đối sánh mẫu đơn giản cho phép nhà phát triển thư viện phát hiện một số mẫu mã đơn giản và thay thế chúng bằng các mã khác nhau. Và việc triển khai GHC của thư viện chuẩn Haskell chứa một số chú thích đó, do đó, việc sử dụng cụ thể các chức năng cụ thể được biết là không hiệu quả được viết lại thành các phiên bản hiệu quả hơn.

Tuy nhiên, những bản dịch này được viết bởi con người, và chúng được viết cho các trường hợp cụ thể, đó là lý do tại sao tôi coi đó là gian lận.

Một siêu trình biên dịch có thể có thể thay đổi thuật toán mà không cần đầu vào của con người, nhưng AFAIK không có siêu trình biên dịch cấp sản xuất nào được chế tạo.


Cảm ơn ví dụ tuyệt vời, và đã đề cập đến GHC. Thêm một câu hỏi: Điều gì về Thực hiện Ra lệnh. Có ví dụ nào được biết đến khi loại tối ưu hóa này dẫn đến thay đổi lớp phức tạp của thuật toán không?
Lorenz Lo Sauer

0

Một trình biên dịch nhận thức được rằng ngôn ngữ đang sử dụng big-num làm giảm sức mạnh (thay thế các phép nhân bằng chỉ số của một vòng lặp bằng một phép cộng) sẽ thay đổi độ phức tạp của phép nhân đó từ O (n log n) tốt nhất thành O (n) .

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.