Tại sao GCC không thể cho rằng std :: vector :: size sẽ không thay đổi trong vòng lặp này?


14

Tôi đã tuyên bố với một đồng nghiệp if (i < input.size() - 1) print(0);sẽ được tối ưu hóa trong vòng lặp này để nó input.size()không được đọc trong mỗi lần lặp, nhưng hóa ra đó không phải là trường hợp!

void print(int x) {
    std::cout << x << std::endl;
}

void print_list(const std::vector<int>& input) {
    int i = 0;
    for (size_t i = 0; i < input.size(); i++) {
        print(input[i]);
        if (i < input.size() - 1) print(0);
    }
}

Theo Compiler Explorer với các tùy chọn gcc, -O3 -fno-exceptionschúng tôi thực sự đang đọc input.size()từng vòng lặp và sử dụng leađể thực hiện phép trừ!

        movq    0(%rbp), %rdx
        movq    8(%rbp), %rax
        subq    %rdx, %rax
        sarq    $2, %rax
        leaq    -1(%rax), %rcx
        cmpq    %rbx, %rcx
        ja      .L35
        addq    $1, %rbx

Thật thú vị, trong Rust tối ưu hóa này xảy ra. Dường như iđược thay thế bằng một biến jđược giảm dần mỗi lần lặp và thử nghiệm i < input.size() - 1được thay thế bằng một cái gì đó như thế j > 0.

fn print(x: i32) {
    println!("{}", x);
}

pub fn print_list(xs: &Vec<i32>) {
    for (i, x) in xs.iter().enumerate() {
        print(*x);
        if i < xs.len() - 1 {
            print(0);
        }
    }
}

Trong Compiler Explorer , hội đồng có liên quan trông như thế này:

        cmpq    %r12, %rbx
        jae     .LBB0_4

Tôi đã kiểm tra và tôi khá chắc chắn r12xs.len() - 1rbxlà quầy. Trước đó có một addfor rbxmovbên ngoài của vòng lặp vào r12.

Tại sao lại thế này? Có vẻ như nếu GCC có thể nội tuyến size()operator[]như đã làm, nó sẽ có thể biết rằng điều size()đó không thay đổi. Nhưng có lẽ trình tối ưu hóa của GCC đánh giá rằng không đáng để kéo nó ra thành một biến? Hoặc có thể có một số tác dụng phụ có thể khác sẽ làm cho điều này không an toàn - có ai biết không?


1
Cũng printlncó thể là một phương thức phức tạp, trình biên dịch có thể gặp khó khăn khi chứng minh rằng printlnkhông làm biến đổi vectơ.
Vịt mướp

1
@MooingDuck: Một chủ đề khác sẽ là cuộc đua dữ liệu UB. Trình biên dịch có thể và giả định rằng điều đó không xảy ra. Vấn đề ở đây là gọi hàm không nội tuyến cout.operator<<(). Trình biên dịch không biết rằng hàm hộp đen này không nhận được tham chiếu đến std::vectortừ toàn cầu.
Peter Cordes

@PeterCordes: bạn đúng khi các chủ đề khác không phải là một lời giải thích độc lập và độ phức tạp của printlnhoặc operator<<là chính.
Vịt mướp

Trình biên dịch không biết ngữ nghĩa của các phương thức bên ngoài này.
dùng207421

Câu trả lời:


10

Hàm gọi phi tuyến cout.operator<<(int)là một hộp đen cho trình tối ưu hóa (vì thư viện chỉ được viết bằng C ++ và tất cả các trình tối ưu hóa nhìn thấy là một nguyên mẫu; xem thảo luận trong các bình luận). Nó phải giả sử bất kỳ bộ nhớ nào có thể được chỉ ra bởi một var toàn cầu đã được sửa đổi.

(Hoặc std::endlcuộc gọi. BTW, tại sao lại buộc một dòng cout vào thời điểm đó thay vì chỉ in một '\n'?)

ví dụ với tất cả những gì nó biết, std::vector<int> &inputlà một tham chiếu đến một biến toàn cục và một trong những lệnh gọi hàm đó sửa đổi var toàn cục đó . (Hoặc có một toàn cầu vector<int> *ptrở đâu đó, hoặc có một hàm trả về một con trỏ đến một static vector<int>đơn vị biên dịch khác, hoặc một cách khác mà một hàm có thể có được một tham chiếu đến vectơ này mà không được chúng ta chuyển qua tham chiếu đến nó.

Nếu bạn có một biến cục bộ có địa chỉ chưa bao giờ được thực hiện, trình biên dịch có thể cho rằng các lệnh gọi hàm không nội tuyến không thể thay đổi nó. Bởi vì sẽ không có cách nào để bất kỳ biến toàn cục nào giữ một con trỏ tới đối tượng này. ( Đây được gọi là Phân tích Thoát ). Đó là lý do tại sao trình biên dịch có thể giữ size_t imột thanh ghi trong các lệnh gọi hàm. ( int ichỉ có thể được tối ưu hóa vì nó bị che khuất size_t ivà không được sử dụng theo cách khác).

Nó có thể làm tương tự với một cục bộ vector(tức là đối với các con trỏ cơ sở, end_size và end_capacity.)

ISO C99 có một giải pháp cho vấn đề này : int *restrict foo. Nhiều biên dịch C ++ hỗ trợ int *__restrict foođể hứa rằng bộ nhớ được trỏ đến bởi foochỉ truy cập thông qua con trỏ đó. Phổ biến nhất là hữu ích trong các hàm có 2 mảng và bạn muốn hứa với trình biên dịch chúng không trùng nhau. Vì vậy, nó có thể tự động vector hóa mà không cần tạo mã để kiểm tra điều đó và chạy một vòng dự phòng.

Ý kiến ​​của OP:

Trong Rust, một tham chiếu không thể thay đổi là một đảm bảo toàn cầu mà không ai khác đang làm thay đổi giá trị mà bạn có tham chiếu (tương đương với C ++ restrict)

Điều đó giải thích tại sao Rust có thể thực hiện tối ưu hóa này nhưng C ++ thì không.


Tối ưu hóa C ++ của bạn

Rõ ràng bạn nên sử dụng auto size = input.size();một lần ở đầu hàm để trình biên dịch biết đó là bất biến vòng lặp. Việc triển khai C ++ không giải quyết được vấn đề này cho bạn, vì vậy bạn phải tự làm điều đó.

Bạn cũng có thể cần const int *data = input.data();phải nâng tải con trỏ dữ liệu từ std::vector<int>"khối điều khiển". Thật không may khi tối ưu hóa có thể yêu cầu thay đổi nguồn rất không thành ngữ.

Rust là một ngôn ngữ hiện đại hơn nhiều, được thiết kế sau khi các nhà phát triển trình biên dịch tìm hiểu những gì có thể trong thực tế cho trình biên dịch. Nó thực sự chương trình theo những cách khác nữa, bao gồm portably phơi bày một số mát CPU thứ có thể làm qua i32.count_ones, xoay, bit quét, vv Đó là thực sự câm mà ISO C ++ vẫn không tiếp xúc với bất kỳ của các portably, ngoại trừ std::bitset::count().


1
Mã của OP vẫn có bài kiểm tra nếu vectơ được lấy theo giá trị. Vì vậy, mặc dù GCC có thể tối ưu hóa trong trường hợp đó, nhưng nó không làm như vậy.
quả óc chó

1
Tiêu chuẩn xác định hành vi của operator<<các loại toán hạng đó; Vì vậy, trong Standard C ++, nó không phải là hộp đen và trình biên dịch có thể cho rằng nó thực hiện những gì tài liệu nói. Có lẽ họ muốn hỗ trợ các nhà phát triển thư viện thêm hành vi phi tiêu chuẩn ...
MM

2
Trình tối ưu hóa có thể được cung cấp hành vi mà các tiêu chuẩn bắt buộc, quan điểm của tôi là tối ưu hóa này được tiêu chuẩn cho phép nhưng nhà cung cấp trình biên dịch chọn thực hiện theo cách bạn mô tả và từ bỏ tối ưu hóa này
MM

2
@MM Nó không nói đối tượng ngẫu nhiên, tôi đã nói một vectơ xác định thực hiện. Không có gì trong tiêu chuẩn cấm thực thi có một vectơ xác định thực hiện mà toán tử << sửa đổi và cho phép truy cập vectơ này theo cách xác định thực hiện. coutcho phép một đối tượng của lớp do người dùng định nghĩa xuất phát từ streambufđược liên kết với luồng sử dụng cout.rdbuf. Tương tự như vậy một đối tượng có nguồn gốc từ ostreamcó thể được liên kết với cout.tie.
Ross Ridge

2
@PeterCordes - Tôi sẽ không tự tin về các vectơ cục bộ: ngay khi bất kỳ hàm thành viên nào bị lỗi, người dân địa phương đã thoát một cách hiệu quả vì thiscon trỏ được thông qua một cách ngầm định. Điều này có thể xảy ra trong thực tế sớm nhất là nhà xây dựng. Hãy xem xét vòng lặp đơn giản này - Tôi chỉ kiểm tra vòng lặp chính gcc (từ L34:đến jne L34), nhưng nó chắc chắn hoạt động như thể các thành viên vectơ đã thoát (tải chúng từ bộ nhớ mỗi lần lặp).
BeeOnRope
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.