Sự khác biệt lớn (x9) về thời gian thực thi giữa mã gần như giống hệt nhau trong C và C ++


85

Tôi đang cố giải bài tập này từ www.spoj.com: FCTRL - Giai thừa

Bạn không thực sự phải đọc nó, chỉ cần làm điều đó nếu bạn tò mò :)

Đầu tiên tôi triển khai nó bằng C ++ (đây là giải pháp của tôi):

#include <iostream>
using namespace std;

int main() {
    unsigned int num_of_inputs;
    unsigned int fact_num;
    unsigned int num_of_trailing_zeros;

    std::ios_base::sync_with_stdio(false); // turn off synchronization with the C library’s stdio buffers (from https://stackoverflow.com/a/22225421/5218277)

    cin >> num_of_inputs;

    while (num_of_inputs--)
    {
        cin >> fact_num;

        num_of_trailing_zeros = 0;

        for (unsigned int fives = 5; fives <= fact_num; fives *= 5)
            num_of_trailing_zeros += fact_num/fives;

        cout << num_of_trailing_zeros << "\n";
    }

    return 0;
}

Tôi đã tải nó lên làm giải pháp cho g ++ 5.1

Kết quả là: Thời gian 0,18 Mem 3,3M Kết quả thực thi C ++

Nhưng sau đó tôi thấy một số bình luận cho rằng thời gian thực thi của chúng nhỏ hơn 0,1. Vì tôi không thể nghĩ về thuật toán nhanh hơn, tôi đã cố gắng triển khai cùng một đoạn mã trong C :

#include <stdio.h>

int main() {
    unsigned int num_of_inputs;
    unsigned int fact_num;
    unsigned int num_of_trailing_zeros;

    scanf("%d", &num_of_inputs);

    while (num_of_inputs--)
    {
        scanf("%d", &fact_num);

        num_of_trailing_zeros = 0;

        for (unsigned int fives = 5; fives <= fact_num; fives *= 5)
            num_of_trailing_zeros += fact_num/fives;

        printf("%d", num_of_trailing_zeros);
        printf("%s","\n");
    }

    return 0;
}

Tôi đã tải nó lên làm giải pháp cho gcc 5.1

Lần này kết quả là: Thời gian 0,02 Mem 2,1M Kết quả thực hiện C

Bây giờ mã gần như giống nhau , tôi đã thêm vào std::ios_base::sync_with_stdio(false);mã C ++ như được đề xuất ở đây để tắt đồng bộ hóa với bộ đệm stdio của thư viện C. Tôi cũng chia printf("%d\n", num_of_trailing_zeros);đến printf("%d", num_of_trailing_zeros); printf("%s","\n");để bù đắp cho cuộc gọi gấp đôi operator<<trong cout << num_of_trailing_zeros << "\n";.

Nhưng tôi vẫn thấy x9 hiệu suất tốt hơn và sử dụng bộ nhớ thấp hơn trong mã C so với C ++.

Tại sao vậy?

BIÊN TẬP

Tôi đã sửa unsigned longthành unsigned intmã C. Đáng lẽ ra unsigned int, kết quả được hiển thị ở trên có liên quan đến unsigned intphiên bản ( ) mới.


31
Các luồng C ++ cực kỳ chậm theo thiết kế. Bởi vì chậm và ổn định sẽ chiến thắng cuộc đua. : P ( chạy trước khi tôi nổi lửa )
Mysticial

7
Sự chậm chạp không đến từ sự an toàn hay khả năng thích ứng. Nó được thiết kế quá mức với tất cả các cờ luồng.
Karoly Horvath

8
@AlexLop. Sử dụng a std::ostringstreamđể tích lũy đầu ra và gửi nó đến std::cout tất cả cùng một lúc ở cuối sẽ làm giảm thời gian xuống 0,02. Sử dụng std::couttrong một vòng lặp chỉ đơn giản là chậm hơn trong môi trường của họ và tôi không nghĩ có cách đơn giản để cải thiện nó.
Blastfurnace

6
Không ai khác lo ngại về thực tế là những thời gian này được thu thập bằng cách sử dụng Ideone?
ildjarn

6
@Olaf: Tôi e rằng tôi không đồng ý, loại câu hỏi này rất chủ đề cho tất cả các thẻ đã chọn. Nói chung, C và C ++ đủ gần nhau nên sự khác biệt về hiệu suất như vậy đòi hỏi một lời giải thích. Tôi rất vui vì chúng tôi đã tìm thấy nó. Có lẽ nên cải tiến GNU libc ++.
chqrlie

Câu trả lời:


56

Cả hai chương trình đều làm chính xác những điều tương tự. Chúng sử dụng cùng một thuật toán chính xác và với độ phức tạp thấp, hiệu suất của chúng chủ yếu phụ thuộc vào hiệu quả của việc xử lý đầu vào và đầu ra.

quét đầu vào bằng scanf("%d", &fact_num);một mặt và cin >> fact_num;mặt khác dường như không tốn kém nhiều. Trong thực tế, nó sẽ ít tốn kém hơn trong C ++ vì kiểu chuyển đổi được biết đến tại thời điểm biên dịch và trình phân tích cú pháp chính xác có thể được gọi trực tiếp bởi trình biên dịch C ++. Điều tương tự đối với đầu ra. Bạn thậm chí còn viết một lời gọi riêng cho printf("%s","\n");, nhưng trình biên dịch C đủ tốt để biên dịch nó thành một lời gọi tới putchar('\n');.

Vì vậy, nhìn vào độ phức tạp của cả I / O và tính toán, phiên bản C ++ sẽ nhanh hơn phiên bản C.

Việc vô hiệu hóa hoàn toàn bộ đệm stdoutlàm chậm quá trình triển khai C xuống một thứ thậm chí còn chậm hơn phiên bản C ++. Một thử nghiệm khác của AlexLop với dấu fflush(stdout);sau lần cuối cùng printfmang lại hiệu suất tương tự như phiên bản C ++. Nó không chậm như vô hiệu hóa hoàn toàn bộ đệm vì đầu ra được ghi vào hệ thống theo từng phần nhỏ thay vì từng byte một.

Điều này dường như chỉ ra một hành vi cụ thể trong thư viện C ++ của bạn: Tôi nghi ngờ việc triển khai hệ thống của bạn cincoutxóa đầu ra coutkhi đầu vào được yêu cầu cin. Một số thư viện C cũng làm điều này, nhưng thường chỉ khi đọc / ghi đến và từ thiết bị đầu cuối. Việc đo điểm chuẩn được thực hiện bởi trang www.spoj.com có ​​thể chuyển hướng đầu vào và đầu ra đến và từ các tệp.

AlexLop đã thực hiện một thử nghiệm khác: đọc tất cả các đầu vào cùng một lúc trong một vectơ và sau đó tính toán và ghi tất cả đầu ra giúp hiểu tại sao phiên bản C ++ lại chậm hơn nhiều. Nó làm tăng hiệu suất so với phiên bản C, điều này chứng minh quan điểm của tôi và loại bỏ sự nghi ngờ về mã định dạng C ++.

Một thử nghiệm khác của Blastfurnace, lưu trữ tất cả các kết quả đầu ra trong một std::ostringstreamvà xả ra trong một lần thổi cuối cùng, cải thiện hiệu suất C ++ so với phiên bản C cơ bản. QED.

Việc xen kẽ đầu vào từ cinvà đầu ra đến coutdường như gây ra việc xử lý I / O rất kém hiệu quả, đánh bại sơ đồ đệm luồng. giảm hiệu suất theo hệ số 10.

Tái bút: thuật toán của bạn không chính xác fact_num >= UINT_MAX / 5fives *= 5sẽ tràn và quấn quanh trước khi nó trở thành > fact_num. Bạn có thể sửa lỗi này bằng cách tạo fivesmột unsigned longhoặc một unsigned long longnếu một trong các loại này lớn hơn unsigned int. Cũng sử dụng %ulàm scanfđịnh dạng. Bạn thật may mắn khi các chàng trai tại www.spoj.com không quá khắt khe về điểm chuẩn của họ.

CHỈNH SỬA: Như sau này vitaux giải thích, hành vi này thực sự được yêu cầu bởi tiêu chuẩn C ++. cinđược gắn với couttheo mặc định. Một hoạt động đầu vào cinmà từ đó bộ đệm đầu vào cần nạp lại sẽ gây coutra đầu ra đang chờ xử lý. Trong quá trình triển khai của OP, cindường như tuôn ra coutmột cách có hệ thống, điều này hơi quá mức cần thiết và rõ ràng là không hiệu quả.

Ilya Popov đã đưa ra một giải pháp đơn giản cho điều này: cincó thể được cởi trói coutbằng cách sử dụng một phép thuật ma thuật khác ngoài std::ios_base::sync_with_stdio(false);:

cin.tie(nullptr);

Cũng lưu ý rằng việc xả nước cưỡng bức như vậy cũng xảy ra khi sử dụng std::endlthay vì '\n'để tạo ra kết thúc dòng cout. Việc thay đổi dòng đầu ra thành giao diện tưởng tượng và ngây thơ hơn trong C ++ cout << num_of_trailing_zeros << endl;sẽ làm giảm hiệu suất theo cùng một cách.


2
Có lẽ bạn đã đúng về việc xả luồng. Thu thập kết quả đầu ra trong a std::ostringstreamvà xuất tất cả một lần vào cuối sẽ đưa thời gian xuống ngang bằng với phiên bản C.
Blastfurnace

2
@ DavidC.Rankin: Tôi đã mạo hiểm phỏng đoán (cout bị đỏ bừng khi đọc cin), nghĩ ra cách để chứng minh điều đó, AlexLop đã thực hiện nó và nó đưa ra bằng chứng thuyết phục, nhưng Blastfurnace đã nghĩ ra một cách khác để chứng minh quan điểm của tôi và các bài kiểm tra của anh ấy đưa ra những bằng chứng thuyết phục không kém. Tôi lấy nó để làm bằng chứng, nhưng tất nhiên nó không phải là một bằng chứng hoàn toàn chính thức, nhìn vào mã nguồn C ++ thì có thể.
chqrlie

2
Tôi đã thử sử dụng ostringstreamcho đầu ra và nó cho Thời gian là 0,02 QED :). Về điểm fact_num >= UINT_MAX / 5, điểm TỐT!
Alex Lop.

1
Thu thập tất cả các đầu vào thành a vectorvà sau đó xử lý các phép tính (không có ostringstream) cho cùng một kết quả! Thời gian 0,02. Kết hợp cả hai vectorostringstreamkhông cải thiện nó nhiều hơn. Đồng thời 0,02
Alex Lop.

2
Một sửa chữa đơn giản mà làm việc ngay cả khi sizeof(int) == sizeof(long long)là thế này: thêm một bài kiểm tra trong cơ thể của vòng lặp sau num_of_trailing_zeros += fact_num/fives;để kiểm tra nếu fivesđã đạt đến mức tối đa của nó:if (fives > UINT_MAX / 5) break;
chqrlie

44

Một mẹo khác để làm cho iostreamtốc độ nhanh hơn khi bạn sử dụng cả hai cincoutlà gọi

cin.tie(nullptr);

Theo mặc định, khi bạn nhập bất cứ thứ gì từ cin, nó sẽ tự động xóa cout. Nó có thể gây hại đáng kể đến hiệu suất nếu bạn thực hiện đầu vào và đầu ra xen kẽ. Điều này được thực hiện đối với giao diện dòng lệnh sử dụng, nơi bạn hiển thị một số lời nhắc và sau đó đợi dữ liệu:

std::string name;
cout << "Enter your name:";
cin >> name;

Trong trường hợp này, bạn muốn đảm bảo rằng lời nhắc thực sự được hiển thị trước khi bạn bắt đầu đợi nhập liệu. Với dòng trên, bạn phá vỡ ràng buộc đó cincouttrở nên độc lập.

Kể từ C ++ 11, một cách nữa để đạt được hiệu suất tốt hơn với iostreams là sử dụng std::getlinecùng với std::stoi, như sau:

std::string line;
for (int i = 0; i < n && std::getline(std::cin, line); ++i)
{
    int x = std::stoi(line);
}

Cách này có thể gần với kiểu C về hiệu suất, hoặc thậm chí vượt qua scanf. Sử dụng getcharvà đặc biệt là getchar_unlockedcùng với phân tích cú pháp viết tay vẫn mang lại hiệu suất tốt hơn.

Tái bút. Tôi đã viết một bài so sánh một số cách nhập số trong C ++, hữu ích cho các thẩm phán trực tuyến, nhưng nó chỉ bằng tiếng Nga, xin lỗi. Tuy nhiên, các mẫu mã và bảng cuối cùng phải dễ hiểu.


1
Cảm ơn bạn đã giải thích và +1 cho giải pháp, nhưng giải pháp thay thế được đề xuất của bạn có std::readlinestd::stoikhông tương đương về mặt chức năng với mã OP. Cả hai cin >> x;scanf("%f", &x);chấp nhận khoảng trắng con kiến ​​làm dấu phân cách, có thể có nhiều số trên cùng một dòng.
chqrlie

27

Vấn đề là, trích dẫn cppreference :

bất kỳ đầu vào nào từ std :: cin, đầu ra đến std :: cerr hoặc kết thúc chương trình buộc một lệnh gọi đến std :: cout.flush ()

Điều này rất dễ kiểm tra: nếu bạn thay thế

cin >> fact_num;

với

scanf("%d", &fact_num);

và tương tự cin >> num_of_inputsnhưng coutbạn sẽ nhận được khá nhiều hiệu suất tương tự trong phiên bản C ++ của mình (hoặc đúng hơn là phiên bản IOStream) như trong C one:

nhập mô tả hình ảnh ở đây

Điều tương tự cũng xảy ra nếu bạn giữ lại cinnhưng thay thế

cout << num_of_trailing_zeros << "\n";

với

printf("%d", num_of_trailing_zeros);
printf("%s","\n");

Một giải pháp đơn giản là cởi trói coutcinnhư Ilya Popov đã đề cập:

cin.tie(nullptr);

Việc triển khai thư viện chuẩn được phép bỏ qua lệnh gọi tuôn ra trong một số trường hợp nhất định, nhưng không phải lúc nào cũng vậy. Đây là trích dẫn từ C ++ 14 27.7.2.1.3 (cảm ơn chqrlie):

Lớp basic_istream :: sentry: Đầu tiên, nếu is.tie () không phải là một con trỏ null, hàm gọi is.tie () -> flush () để đồng bộ hóa chuỗi đầu ra với bất kỳ luồng C bên ngoài nào được liên kết. Ngoại trừ việc lệnh gọi này có thể bị chặn nếu vùng đặt của is.tie () trống. Hơn nữa, một triển khai được phép trì hoãn cuộc gọi tuôn ra cho đến khi một lệnh gọi is.rdbuf () -> underflow () xảy ra. Nếu không có lệnh gọi nào như vậy xảy ra trước khi đối tượng sentry bị hủy, lệnh gọi xả có thể bị loại bỏ hoàn toàn.


Cảm ơn vì lời giải thích. Tuy nhiên, trích dẫn C ++ 14 27.7.2.1.3: Lớp basic_istream :: sentry : Đầu tiên, nếu is.tie()không phải là con trỏ null, hàm gọi is.tie()->flush()để đồng bộ hóa chuỗi đầu ra với bất kỳ luồng C bên ngoài nào được liên kết. Ngoại trừ việc lệnh gọi này có thể bị chặn nếu vùng đặt is.tie()trống. Hơn nữa, một triển khai được phép trì hoãn lệnh gọi cho đến khi lệnh gọi is.rdbuf()->underflow()xảy ra. Nếu không có lệnh gọi nào như vậy xảy ra trước khi đối tượng sentry bị hủy, lệnh gọi xả có thể bị loại bỏ hoàn toàn.
chqrlie

Như thường lệ với C ++, mọi thứ phức tạp hơn vẻ ngoài của chúng. Thư viện C ++ của OP không hiệu quả như Tiêu chuẩn cho phép.
chqrlie

Cảm ơn vì liên kết cppreference. Tôi không thích "câu trả lời sai" trong màn hình in mặc dù ☺
Alex Lop.

@AlexLop. Rất tiếc, đã sửa lỗi "câu trả lời sai" =). Quên cập nhật cin khác (mặc dù điều này không ảnh hưởng đến thời gian).
vitaut

@chqrlie Đúng, nhưng ngay cả trong trường hợp dòng chảy, hiệu suất vẫn có thể kém hơn so với giải pháp stdio. Cảm ơn vì tiêu chuẩn giới thiệu.
vitaut
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.