ทำไมการอ่านบรรทัดจาก stdin ช้ากว่ามากใน C ++ มากกว่า Python?


1839

ฉันต้องการเปรียบเทียบบรรทัดการอ่านของอินพุตสตริงจาก stdin โดยใช้ Python และ C ++ และรู้สึกตกใจเมื่อเห็นว่ารหัส C ++ ของฉันมีลำดับความสำคัญช้ากว่าโค้ด Python ที่เทียบเท่า เนื่องจาก C ++ ของฉันเป็นสนิมและฉันยังไม่ใช่ผู้เชี่ยวชาญ Pythonista โปรดบอกฉันว่าฉันทำอะไรผิดหรือถ้าฉันเข้าใจผิดบางอย่าง


(คำตอบ TLDR: รวมคำสั่ง: cin.sync_with_stdio(false)หรือใช้fgetsแทน

ผลลัพธ์ TLDR: เลื่อนลงมาจนสุดด้านล่างของคำถามแล้วดูที่ตาราง)


รหัส C ++:

#include <iostream>
#include <time.h>

using namespace std;

int main() {
    string input_line;
    long line_count = 0;
    time_t start = time(NULL);
    int sec;
    int lps;

    while (cin) {
        getline(cin, input_line);
        if (!cin.eof())
            line_count++;
    };

    sec = (int) time(NULL) - start;
    cerr << "Read " << line_count << " lines in " << sec << " seconds.";
    if (sec > 0) {
        lps = line_count / sec;
        cerr << " LPS: " << lps << endl;
    } else
        cerr << endl;
    return 0;
}

// Compiled with:
// g++ -O3 -o readline_test_cpp foo.cpp

หลามเทียบเท่า:

#!/usr/bin/env python
import time
import sys

count = 0
start = time.time()

for line in  sys.stdin:
    count += 1

delta_sec = int(time.time() - start_time)
if delta_sec >= 0:
    lines_per_sec = int(round(count/delta_sec))
    print("Read {0} lines in {1} seconds. LPS: {2}".format(count, delta_sec,
       lines_per_sec))

นี่คือผลลัพธ์ของฉัน:

$ cat test_lines | ./readline_test_cpp
Read 5570000 lines in 9 seconds. LPS: 618889

$cat test_lines | ./readline_test.py
Read 5570000 lines in 1 seconds. LPS: 5570000

ฉันควรทราบว่าฉันลองทั้งสองอย่างภายใต้ Mac OS X v10.6.8 (Snow Leopard) และ Linux 2.6.32 (Red Hat Linux 6.2) อดีตคือ MacBook Pro และรุ่นหลังเป็นเซิร์ฟเวอร์ที่มีเนื้อมากไม่ใช่ว่ามันเกี่ยวข้องกันมากเกินไป

$ for i in {1..5}; do echo "Test run $i at `date`"; echo -n "CPP:"; cat test_lines | ./readline_test_cpp ; echo -n "Python:"; cat test_lines | ./readline_test.py ; done
Test run 1 at Mon Feb 20 21:29:28 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 2 at Mon Feb 20 21:29:39 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 3 at Mon Feb 20 21:29:50 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 4 at Mon Feb 20 21:30:01 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 5 at Mon Feb 20 21:30:11 EST 2012
CPP:   Read 5570001 lines in 10 seconds. LPS: 557000
Python:Read 5570000 lines in  1 seconds. LPS: 5570000

ภาคผนวกมาตรฐานเล็ก ๆ และสรุป

เพื่อความสมบูรณ์ฉันคิดว่าฉันจะอัปเดตความเร็วการอ่านสำหรับไฟล์เดียวกันบนกล่องเดียวกันด้วยรหัส C ++ เดิม (ซิงค์) นี่เป็นไฟล์บรรทัด 100M บนดิสก์ที่รวดเร็ว นี่คือการเปรียบเทียบด้วยโซลูชัน / แนวทางหลายประการ:

Implementation      Lines per second
python (default)           3,571,428
cin (default/naive)          819,672
cin (no sync)             12,500,000
fgets                     14,285,714
wc (not fair comparison)  54,644,808

14
คุณรันการทดสอบหลายครั้งหรือไม่? อาจมีปัญหาแคชดิสก์
Vaughn Cato

9
@JJC: ฉันเห็นความเป็นไปได้สองอย่าง (สมมติว่าคุณลบปัญหาแคชที่แนะนำโดย David): 1) <iostream>ประสิทธิภาพแย่ ไม่ใช่ครั้งแรกที่มันเกิดขึ้น 2) Python ฉลาดพอที่จะไม่คัดลอกข้อมูลใน for for loop เพราะคุณไม่ได้ใช้งาน คุณสามารถสอบซ่อมพยายามที่จะใช้และscanf char[]หรือคุณอาจลองเขียนลูปใหม่เพื่อให้บางสิ่งบางอย่างเสร็จสิ้นด้วยสตริง (เช่นเก็บตัวอักษรตัวที่ 5 แล้วต่อกันเป็นผล)
JN

15
ปัญหาคือการประสานกับ stdio - ดูคำตอบของฉัน
Vaughn Cato

19
ดูเหมือนจะไม่มีใครพูดถึงว่าทำไมคุณถึงได้รับสายเพิ่มเติมด้วย C ++: อย่าทดสอบกับcin.eof()!! วางgetlineสายลงในคำสั่ง 'if`
Xeo

21
wc -lเร็วเพราะอ่านกระแสมากกว่าหนึ่งบรรทัดในแต่ละครั้ง (อาจเป็นการfread(stdin)/memchr('\n')รวมกัน) ผลลัพธ์ของ Python นั้นมีขนาดเท่ากันเช่น,wc-l.py
jfs

คำตอบ:


1644

โดยค่าเริ่มต้นcinจะซิงโครไนซ์กับ stdio ซึ่งทำให้หลีกเลี่ยงการบัฟเฟอร์ข้อมูลใด ๆ หากคุณเพิ่มสิ่งนี้ไว้ที่ด้านบนของหน้าหลักคุณควรเห็นประสิทธิภาพที่ดีขึ้น:

std::ios_base::sync_with_stdio(false);

โดยปกติเมื่อบัฟเฟอร์สตรีมอินพุตแทนที่จะอ่านหนึ่งตัวอักษรในแต่ละครั้งสตรีมจะถูกอ่านเป็นกลุ่มขนาดใหญ่ สิ่งนี้จะลดจำนวนการเรียกของระบบซึ่งโดยทั่วไปจะค่อนข้างแพง อย่างไรก็ตามเนื่องจากFILE*พื้นฐานstdioและiostreamsมักจะมีการใช้งานแยกต่างหากและดังนั้นจึงแยกบัฟเฟอร์นี้อาจนำไปสู่ปัญหาหากทั้งสองถูกใช้ร่วมกัน ตัวอย่างเช่น:

int myvalue1;
cin >> myvalue1;
int myvalue2;
scanf("%d",&myvalue2);

หากมีการอ่านอินพุตcinมากกว่าที่ต้องการจริงค่าจำนวนเต็มที่สองจะไม่พร้อมใช้งานสำหรับscanfฟังก์ชันซึ่งมีบัฟเฟอร์อิสระของตัวเอง สิ่งนี้จะนำไปสู่ผลลัพธ์ที่ไม่คาดคิด

stdioเพื่อหลีกเลี่ยงนี้โดยค่าเริ่มต้นลำธารตรงกับ วิธีทั่วไปในการบรรลุเป้าหมายนี้คือการcinอ่านอักขระแต่ละตัวทีละตัวตามต้องการโดยใช้stdioฟังก์ชั่น น่าเสียดายที่นี่แนะนำค่าใช้จ่ายจำนวนมาก สำหรับอินพุตจำนวนเล็กน้อยนี่ไม่ใช่ปัญหาใหญ่ แต่เมื่อคุณอ่านหลายล้านบรรทัดการลงโทษประสิทธิภาพถือว่ามีความสำคัญ

โชคดีที่นักออกแบบห้องสมุดตัดสินใจว่าคุณควรปิดใช้งานคุณลักษณะนี้เพื่อรับประสิทธิภาพที่ดีขึ้นหากคุณรู้ว่าคุณกำลังทำอะไรอยู่ดังนั้นพวกเขาจึงให้sync_with_stdioวิธีการนี้


142
สิ่งนี้ควรอยู่ด้านบนสุด มันเกือบจะถูกต้องแน่นอน คำตอบไม่สามารถแทนที่การอ่านด้วยการfscanfโทรได้เพราะนั่นค่อนข้างง่ายที่จะไม่ทำงานเท่าที่งูเหลือมทำ งูหลามต้องจัดสรรหน่วยความจำสำหรับสตริงอาจจะเป็นหลาย ๆ ครั้งการจัดสรรที่มีอยู่จะถือว่าไม่เพียงพอ - เหมือนที่ C ++ std::stringวิธีด้วย งานนี้เกือบจะแน่นอนว่า I / O ผูกพันและมีวิธี FUD มากเกินไปเกี่ยวกับค่าใช้จ่ายในการสร้างstd::stringวัตถุใน C ++ หรือใช้<iostream>ในและของตัวเอง
Karl Knechtel

51
ใช่เพิ่มบรรทัดนี้ทันทีเหนือต้นฉบับของฉันในขณะที่วนซ้ำรหัสเกินกว่าแม้แต่หลาม ฉันกำลังจะโพสต์ผลลัพธ์เป็นการแก้ไขครั้งสุดท้าย ขอบคุณอีกครั้ง!
JJC

6
ใช่สิ่งนี้ใช้ได้กับ cout, cerr และ clog ด้วยเช่นกัน
Vaughn Cato

2
หากต้องการทำให้ cout, cin, cerr และ clog เร็วขึ้นให้ทำดังนี้ std :: ios_base :: sync_with_stdio (false);
01100110

56
โปรดทราบว่าsync_with_stdio()เป็นฟังก์ชั่นสมาชิกคงที่และการเรียกใช้ฟังก์ชั่นนี้กับวัตถุสตรีมใด ๆ (เช่นcin) สลับเพื่อเปิดหรือปิดการประสานสำหรับวัตถุ iostream มาตรฐานทั้งหมด
John Zwinck

171

เพิ่งจากความอยากรู้ฉันได้ดูสิ่งที่เกิดขึ้นภายใต้ประทุนและฉันใช้dtruss / straceในการทดสอบแต่ละครั้ง

C ++

./a.out < in
Saw 6512403 lines in 8 seconds.  Crunch speed: 814050

syscalls sudo dtruss -c ./a.out < in

CALL                                        COUNT
__mac_syscall                                   1
<snip>
open                                            6
pread                                           8
mprotect                                       17
mmap                                           22
stat64                                         30
read_nocancel                               25958

หลาม

./a.py < in
Read 6512402 lines in 1 seconds. LPS: 6512402

syscalls sudo dtruss -c ./a.py < in

CALL                                        COUNT
__mac_syscall                                   1
<snip>
open                                            5
pread                                           8
mprotect                                       17
mmap                                           21
stat64                                         29

159

ฉันอยู่ที่นี่ไม่กี่ปีที่ผ่านมา แต่:

ใน 'แก้ไข 4/5/6' ของโพสต์ต้นฉบับคุณกำลังใช้สิ่งก่อสร้าง:

$ /usr/bin/time cat big_file | program_to_benchmark

นี่เป็นวิธีที่ผิดในสองวิธี:

  1. คุณกำลังกำหนดเวลาดำเนินการcatไม่ใช่เป็นเกณฑ์มาตรฐานของคุณ การใช้งาน CPU 'user' และ 'sys' ที่แสดงtimeเป็นของcatไม่ใช่โปรแกรมการเปรียบเทียบของคุณ ยิ่งแย่ไปกว่านั้นเวลา 'ของจริง' ก็ไม่จำเป็นต้องแม่นยำเช่นกัน ขึ้นอยู่กับการใช้งานcatและของท่อในระบบปฏิบัติการในพื้นที่ของคุณเป็นไปได้ที่จะcatเขียนบัฟเฟอร์ยักษ์ตัวสุดท้ายและออกมานานก่อนที่กระบวนการอ่านจะเสร็จสิ้นการทำงาน

  2. การใช้catไม่จำเป็นและในความเป็นจริงต่อต้าน คุณกำลังเพิ่มชิ้นส่วนเคลื่อนไหว หากคุณอยู่ในระบบเก่าที่เพียงพอ (เช่นมี CPU ตัวเดียวและ - ในคอมพิวเตอร์บางรุ่น - I / O เร็วกว่า CPU) - ความจริงที่catทำงานอยู่อาจทำให้สีของผลลัพธ์ดีขึ้นอย่างมาก คุณยังเป็นเรื่องที่สิ่งที่ input และ output บัฟเฟอร์และประมวลผลอื่น ๆcatอาจจะทำ (นี่น่าจะทำให้คุณได้รับรางวัล'Us Use Use Of Cat'ถ้าฉันเป็น Randal Schwartz

การก่อสร้างที่ดีกว่าคือ:

$ /usr/bin/time program_to_benchmark < big_file

ในคำสั่งนี้มันคือเชลล์ที่เปิด big_file ส่งผ่านไปยังโปรแกรมของคุณ (ซึ่งจริงๆtimeแล้วจะเรียกใช้งานโปรแกรมของคุณเป็น subprocess) เป็นตัวบ่งชี้ไฟล์ที่เปิดอยู่แล้ว 100% ของการอ่านไฟล์เป็นความรับผิดชอบของโปรแกรมที่คุณพยายามทำเกณฑ์มาตรฐานอย่างเคร่งครัด สิ่งนี้ทำให้คุณได้อ่านประสิทธิภาพที่แท้จริงโดยปราศจากความยุ่งยาก

ฉันจะพูดถึงสองที่เป็นไปได้ แต่จริง ๆ แล้ว 'แก้ไข' ซึ่งอาจได้รับการพิจารณา (แต่ 'หมายเลข' พวกเขาแตกต่างกันเนื่องจากสิ่งเหล่านี้ไม่ใช่สิ่งที่ผิดในโพสต์ดั้งเดิม):

A. คุณสามารถ 'แก้ไข' สิ่งนี้ได้โดยกำหนดเวลาเฉพาะโปรแกรมของคุณ:

$ cat big_file | /usr/bin/time program_to_benchmark

B. หรือโดยการกำหนดเวลาไปป์ไลน์ทั้งหมด:

$ /usr/bin/time sh -c 'cat big_file | program_to_benchmark'

สิ่งเหล่านี้ผิดด้วยเหตุผลเดียวกับ # 2: พวกเขายังคงใช้งานโดยcatไม่จำเป็น ฉันพูดถึงพวกเขาด้วยเหตุผลไม่กี่:

  • พวกเขาเป็น 'ธรรมชาติ' มากขึ้นสำหรับผู้ที่ไม่พอใจกับสิ่งอำนวยความสะดวกการเปลี่ยนเส้นทาง I / O ของเปลือก POSIX

  • อาจจะมีกรณีที่cat เป็นสิ่งจำเป็น (เช่นไฟล์ที่จะอ่านต้องเรียงลำดับของสิทธิพิเศษในการเข้าถึงและคุณไม่ต้องการที่จะให้สิทธิพิเศษในการเขียนโปรแกรมที่จะได้รับการวัดประสิทธิผล: sudo cat /dev/sda | /usr/bin/time my_compression_test --no-output)

  • ในทางปฏิบัติเกี่ยวกับเครื่องจักรที่ทันสมัยการเพิ่มcatในท่ออาจจะไม่มีผลที่แท้จริง

แต่ฉันบอกว่าสิ่งสุดท้ายที่มีความลังเล หากเราตรวจสอบผลลัพธ์สุดท้ายใน 'แก้ไข 5' -

$ /usr/bin/time cat temp_big_file | wc -l
0.01user 1.34system 0:01.83elapsed 74%CPU ...

- สิ่งนี้อ้างว่าcatใช้ CPU 74% ในระหว่างการทดสอบ และแน่นอน 1.34 / 1.83 อยู่ที่ประมาณ 74% อาจจะเป็น:

$ /usr/bin/time wc -l < temp_big_file

จะใช้เวลาเพียง 0.49 วินาทีที่เหลือ! อาจจะไม่ได้: catที่นี่จะต้องมีค่าใช้จ่ายสำหรับread()ระบบโทรศัพท์ (หรือเทียบเท่า) ที่ได้รับโอนไฟล์จากดิสก์ '(ที่จริงบัฟเฟอร์แคช) wcเช่นเดียวกับท่อเขียนไปส่งพวกเขาไป การทดสอบที่ถูกต้องจะยังคงต้องทำการread()เรียกเหล่านั้น เฉพาะการบันทึกแบบเขียนไปป์และ read-from-pipe เท่านั้นที่จะได้รับการบันทึกและสิ่งเหล่านั้นน่าจะถูก

ถึงกระนั้นฉันคาดการณ์ว่าคุณจะสามารถวัดความแตกต่างระหว่างcat file | wc -lและwc -l < fileและพบความแตกต่างที่เห็นได้ชัดเจน (2 หลักเปอร์เซ็นต์) การทดสอบที่ช้าลงแต่ละครั้งจะต้องจ่ายค่าปรับในเวลาที่แน่นอน ซึ่งจะมีจำนวนน้อยกว่าเวลารวมทั้งหมด

ในความเป็นจริงฉันทำการทดสอบอย่างรวดเร็วด้วยไฟล์ขยะขนาด 1.5 กิกะไบต์บนระบบ Linux 3.13 (Ubuntu 14.04) เพื่อรับผลลัพธ์เหล่านี้ (ซึ่งจริง ๆ แล้วเป็นผลลัพธ์ที่ 'ดีที่สุดใน 3' หลังจากทำการแคชแคชแน่นอน):

$ time wc -l < /tmp/junk
real 0.280s user 0.156s sys 0.124s (total cpu 0.280s)
$ time cat /tmp/junk | wc -l
real 0.407s user 0.157s sys 0.618s (total cpu 0.775s)
$ time sh -c 'cat /tmp/junk | wc -l'
real 0.411s user 0.118s sys 0.660s (total cpu 0.778s)

โปรดสังเกตว่าผลลัพธ์ไปป์ไลน์ทั้งสองอ้างว่าใช้เวลา CPU (ผู้ใช้ + sys) มากกว่าเวลานาฬิกาแขวนจริง นี่เป็นเพราะฉันใช้คำสั่งในตัวของ 'shell (bash)' ซึ่งเป็นที่รู้จักของไพพ์ไลน์ และฉันอยู่ในเครื่องมัลติคอร์ที่กระบวนการแยกในท่อสามารถใช้แกนประมวลผลแยกกันทำให้การสะสมเวลา CPU เร็วกว่าเรียลไทม์ การใช้/usr/bin/timeฉันเห็นเวลา CPU น้อยกว่าเรียลไทม์ - แสดงว่าสามารถใช้เวลาเพียงองค์ประกอบไปป์ไลน์เดียวที่ผ่านไปยังบรรทัดคำสั่ง นอกจากนี้เอาต์พุตของเชลล์ให้มิลลิวินาทีในขณะที่/usr/bin/timeให้ร้อยวินาทีเท่านั้น

ดังนั้นในระดับประสิทธิภาพของwc -lการcatสร้างความแตกต่าง: 409/283 = 1.453 หรือ 45.3% เรียลไทม์มากขึ้นและ 775/280 = 2.768 หรือ CPU ที่มากขึ้น 177% ใช้! ในกล่องทดสอบแบบสุ่มที่เป็นไปได้ของฉัน

ฉันควรเพิ่มว่าอย่างน้อยหนึ่งความแตกต่างที่สำคัญระหว่างการทดสอบรูปแบบเหล่านี้และฉันไม่สามารถพูดได้ว่าเป็นประโยชน์หรือความผิด คุณต้องตัดสินใจเอง:

เมื่อคุณเรียกใช้cat big_file | /usr/bin/time my_programโปรแกรมของคุณได้รับข้อมูลจากท่อที่แม่นยำก้าวที่ส่งมาจากและในชิ้นไม่ใหญ่กว่าที่เขียนโดยcatcat

เมื่อคุณเรียกใช้/usr/bin/time my_program < big_fileโปรแกรมของคุณจะได้รับตัวอธิบายเปิดไฟล์เป็นไฟล์จริง โปรแกรมของคุณ - หรือในหลาย ๆ กรณีห้องสมุด I / O ของภาษาที่เขียน - อาจดำเนินการแตกต่างกันเมื่อนำเสนอด้วยไฟล์ descriptor ที่อ้างอิงไฟล์ปกติ อาจใช้mmap(2)เพื่อแมปไฟล์อินพุตเข้ากับพื้นที่ที่อยู่แทนที่จะใช้การread(2)เรียกของระบบอย่างชัดเจน ความแตกต่างเหล่านี้อาจมีผลต่อผลลัพธ์การวัดประสิทธิภาพของคุณมากกว่าค่าใช้จ่ายในการรันcatไบนารี

แน่นอนว่ามันเป็นผลการเปรียบเทียบที่น่าสนใจหากโปรแกรมเดียวกันทำงานได้แตกต่างกันอย่างมากระหว่างสองกรณี มันแสดงให้เห็นว่าแท้จริงโปรแกรมหรือฉันมัน / O ห้องสมุดจะmmap()ทำอะไรบางอย่างที่น่าสนใจเช่นการใช้ ดังนั้นในทางปฏิบัติอาจเป็นการดีที่จะใช้มาตรฐานทั้งสองวิธี บางทีการลดcatผลลัพธ์ด้วยปัจจัยเล็ก ๆ บางอย่างเพื่อ "ให้อภัย" ค่าใช้จ่ายในการดำเนินการcatเอง


26
ว้าวนั่นช่างลึกซึ้งเหลือเกิน! ในขณะที่ฉันได้รับทราบว่า cat ไม่จำเป็นสำหรับป้อนข้อมูลให้กับ stdin ของโปรแกรมและที่ต้องการ <shell redirect ฉันมักจะติด cat เนื่องจากการไหลของข้อมูลจากซ้ายไปขวาที่วิธีการเดิมเก็บรักษาภาพไว้ เมื่อฉันมีเหตุผลเกี่ยวกับท่อ ความแตกต่างด้านประสิทธิภาพในกรณีเช่นนี้ฉันพบว่าไม่สำคัญ แต่ฉันซาบซึ้งที่คุณให้การศึกษาแก่เราเบล่า
JJC

11
การเปลี่ยนเส้นทางถูกแยกออกจากบรรทัดคำสั่งเชลล์ในระยะเริ่มต้นซึ่งช่วยให้คุณทำอย่างใดอย่างหนึ่งถ้ามันให้ลักษณะที่น่าพอใจมากขึ้นของการไหลจากซ้ายไปขวา: $ < big_file time my_program $ time < big_file my_program นี้ควรทำงานใน POSIX เชลล์ใด ๆ (เช่นไม่ใช่ `csh `และฉันไม่แน่ใจเกี่ยวกับ Exotica เช่น` rc`)
เบลา Lubkin

6
อีกครั้งนอกเหนือจากความแตกต่างของประสิทธิภาพที่เพิ่มขึ้นอาจไม่น่าสนใจเนื่องจากการทำงานแบบไบนารีของ `cat 'ในเวลาเดียวกันคุณกำลังละทิ้งความเป็นไปได้ของโปรแกรมภายใต้การทดสอบที่สามารถ mmap () ไฟล์อินพุต สิ่งนี้สามารถสร้างความแตกต่างอย่างลึกซึ้งในผลลัพธ์ สิ่งนี้เป็นจริงแม้ว่าคุณจะเขียนเกณฑ์มาตรฐานด้วยตัวคุณเองในภาษาต่างๆโดยใช้เพียง 'บรรทัดอินพุตจากไฟล์' สำนวน ขึ้นอยู่กับรายละเอียดการทำงานของไลบรารี I / O ต่างๆ
Bela Lubkin

2
หมายเหตุด้านข้าง: builtin ของ Bash timeกำลังวัดค่าไปป์ไลน์ทั้งหมดแทนที่จะเป็นโปรแกรมแรก time seq 2 | while read; do sleep 1; doneพิมพ์ 2 วินาที/usr/bin/time seq 2 | while read; do sleep 1; doneพิมพ์ 0 วินาที
folkol

1
@folkol - ใช่ << ขอให้สังเกตว่าทั้งสองไปป์ไลน์ผลลัพธ์ [แสดง] CPU มากขึ้น [กว่า] เรียลไทม์ [ใช้] คำสั่งในตัว 'เวลา' ในตัว (Bash); ... / usr / bin / time ... สามารถกำหนดเวลาองค์ประกอบไปป์ไลน์เดียวที่ผ่านไปยังบรรทัดคำสั่งได้ >> '
Bela Lubkin

90

ฉันทำซ้ำผลลัพธ์ดั้งเดิมบนคอมพิวเตอร์โดยใช้ g ++ บน Mac

การเพิ่มข้อความสั่งต่อไปนี้ในเวอร์ชัน C ++ ก่อนที่whileลูปจะนำอินไลน์เข้ากับเวอร์ชันPython :

std::ios_base::sync_with_stdio(false);
char buffer[1048576];
std::cin.rdbuf()->pubsetbuf(buffer, sizeof(buffer));

sync_with_stdio ปรับปรุงความเร็วเป็น 2 วินาทีและการตั้งค่าบัฟเฟอร์ที่ใหญ่ขึ้นทำให้ลดลงเป็น 1 วินาที


5
คุณอาจต้องการลองขนาดบัฟเฟอร์ที่แตกต่างกันเพื่อรับข้อมูลที่เป็นประโยชน์มากขึ้น ฉันสงสัยว่าคุณจะเห็นผลตอบแทนลดลงอย่างรวดเร็ว
Karl Knechtel

8
ฉันรีบร้อนเกินไปในการตอบกลับของฉัน; การตั้งค่าขนาดบัฟเฟอร์เป็นอย่างอื่นที่ไม่ใช่ค่าเริ่มต้นไม่ได้สร้างความแตกต่างที่เห็นได้
karunski

109
ฉันจะหลีกเลี่ยงการตั้งค่าบัฟเฟอร์ 1MB บนสแต็ก มันสามารถนำไปสู่การ stackoverflow ( แต่ผมคิดว่ามันเป็นสถานที่ที่ดีที่จะอภิปรายเกี่ยวกับมัน)
Matthieu เอ็ม

11
Matthieu, Mac ใช้สแต็กกระบวนการ 8MB เป็นค่าเริ่มต้น Linux ใช้ค่าเริ่มต้น 4MB ต่อเธรด IIRC 1MB ไม่ใช่ปัญหาสำหรับโปรแกรมที่แปลงอินพุตด้วยระดับความลึกที่ค่อนข้างตื้น แม้ว่าสิ่งที่สำคัญกว่านั้นคือ std :: cin จะทำการทิ้งสแต็กถ้าบัฟเฟอร์ออกนอกขอบเขต
SEK

22
@SEK ขนาดเริ่มต้นของ Windows คือ 1MB
Étienne

39

getlineตัวดำเนินการสตรีมscanfสามารถทำได้อย่างสะดวกถ้าคุณไม่สนใจเวลาในการโหลดไฟล์หรือถ้าคุณกำลังโหลดไฟล์ข้อความขนาดเล็ก แต่ถ้าประสิทธิภาพเป็นสิ่งที่คุณใส่ใจคุณควรบัฟเฟอร์ไฟล์ทั้งหมดลงในหน่วยความจำจริง ๆ (สมมติว่ามันพอดี)

นี่คือตัวอย่าง:

//open file in binary mode
std::fstream file( filename, std::ios::in|::std::ios::binary );
if( !file ) return NULL;

//read the size...
file.seekg(0, std::ios::end);
size_t length = (size_t)file.tellg();
file.seekg(0, std::ios::beg);

//read into memory buffer, then close it.
char *filebuf = new char[length+1];
file.read(filebuf, length);
filebuf[length] = '\0'; //make it null-terminated
file.close();

หากคุณต้องการคุณสามารถปิดการสตรีมรอบบัฟเฟอร์นั้นเพื่อการเข้าถึงที่สะดวกยิ่งขึ้นเช่นนี้

std::istrstream header(&filebuf[0], length);

นอกจากนี้หากคุณเป็นผู้ควบคุมไฟล์ให้พิจารณาใช้รูปแบบข้อมูลไบนารีแบบแบนแทนข้อความ การอ่านและเขียนมีความน่าเชื่อถือมากขึ้นเพราะคุณไม่ต้องจัดการกับความคลุมเครือของช่องว่างทั้งหมด นอกจากนี้ยังเล็กและเร็วกว่าในการแยกวิเคราะห์


20

รหัสต่อไปนี้เร็วกว่าสำหรับฉันมากกว่ารหัสอื่น ๆ ที่โพสต์ที่นี่จนถึงตอนนี้: (Visual Studio 2013, 64- บิต, 500 MB ไฟล์ที่มีความยาวบรรทัดสม่ำเสมอใน [0, 1,000))

const int buffer_size = 500 * 1024;  // Too large/small buffer is not good.
std::vector<char> buffer(buffer_size);
int size;
while ((size = fread(buffer.data(), sizeof(char), buffer_size, stdin)) > 0) {
    line_count += count_if(buffer.begin(), buffer.begin() + size, [](char ch) { return ch == '\n'; });
}

มันชนะความพยายามของ Python ของฉันมากกว่าปัจจัย 2


คุณจะได้รับได้เร็วขึ้นกว่าที่กำหนดเองที่มีขนาดเล็ก แต่โปรแกรม C ตรงไปตรงมาอย่างสมบูรณ์ที่ซ้ำทำให้ unbuffered ทั้งreadsyscalls เป็นบัฟเฟอร์แบบคงที่ของความยาวBUFSIZEหรือผ่านทางที่สอดคล้องเทียบเท่าmmapsyscalls for (char *cp = buf; *cp; cp++) count += *cp == "\n"และแส้แล้วผ่านการขึ้นบรรทัดใหม่ที่บัฟเฟอร์นับàลา คุณจะต้องปรับแต่งBUFSIZEให้กับระบบของคุณแม้ว่า stdio ตัวใดที่คุณได้ทำไปแล้ว แต่การforวนซ้ำนั้นควรรวบรวมคำสั่งภาษาแอสเซมเบลอร์กรีดร้องที่รวดเร็วอย่างไม่น่าเชื่อสำหรับฮาร์ดแวร์ของกล่องของคุณ
tchrist

3
count_if และแลมบ์ดาก็รวบรวมลงไปที่ "แอสเซมเบลอร์ที่เร็วสุด ๆ "
Petter

17

อย่างไรก็ตามเหตุผลที่การนับบรรทัดสำหรับรุ่น C ++ เป็นหนึ่งมากกว่าการนับสำหรับเวอร์ชัน Python ก็คือการตั้งค่าสถานะ eof จะได้รับการตั้งค่าเฉพาะเมื่อมีความพยายามที่จะอ่านเกิน eof ดังนั้นวงที่ถูกต้องจะเป็น:

while (cin) {
    getline(cin, input_line);

    if (!cin.eof())
        line_count++;
};

70
วงวนที่ถูกต้องจริงๆคือ: while (getline(cin, input_line)) line_count++;
Jonathan Wakely

2
@JonathanWakely ฉันรู้ว่าฉันรักสาย แต่ใช้ไม่ได้++line_count; line_count++;
val พูดว่า Reinstate Monica

7
@val หากคอมไพเลอร์ของคุณมีข้อผิดพลาด ตัวแปรคือ a longและคอมไพเลอร์ค่อนข้างสามารถบอกได้ว่าผลลัพธ์ของการเพิ่มไม่ได้ถูกใช้ หากไม่ได้สร้างรหัสที่เหมือนกันสำหรับการโพสต์ก่อนหน้าและการพรีเมนต์ก่อนหน้านั้นมันเสีย
Jonathan Wakely

2
อันที่จริงใด ๆ คอมไพเลอร์ที่ดีจะสามารถที่จะตรวจสอบในทางที่ผิดโพสต์เพิ่มขึ้นและทดแทนได้โดยก่อนที่เพิ่มขึ้นแทน แต่คอมไพเลอร์ไม่จำเป็นต้อง ดังนั้นไม่มันจะไม่แตกแม้ว่าคอมไพเลอร์จะไม่ทำการทดแทน ยิ่งกว่านั้นการเขียน++line_count;แทนที่จะline_count++;ไม่เจ็บ :)
Fareanor

1
@valsaysReinstateMonica ในตัวอย่างที่เฉพาะเจาะจงนี้เหตุใดจึงเลือกหนึ่งรายการ ผลลัพธ์จะไม่ถูกใช้ที่นี่ทั้งสองวิธีดังนั้นจะอ่านหลังจากwhileนั้นใช่ไหม จะเกิดอะไรขึ้นถ้ามีข้อผิดพลาดบางประเภทและคุณต้องการให้แน่ใจว่าline_countถูกต้องหรือไม่ ฉันแค่เดา ​​แต่ฉันก็ไม่เข้าใจว่าทำไมมันถึงสำคัญ
TankorSmash

14

ในตัวอย่างที่สองของคุณ (ด้วย scanf ()) เหตุผลที่สิ่งนี้ยังช้าลงอาจเป็นเพราะ scanf ("% s") แยกวิเคราะห์สตริงและค้นหา space char ใด ๆ (space, tab, newline)

นอกจากนี้ใช่ CPython ทำการแคชเพื่อหลีกเลี่ยงการอ่านฮาร์ดดิสก์


12

องค์ประกอบแรกของคำตอบ: <iostream>ช้า ประณามช้า ฉันได้รับการเพิ่มประสิทธิภาพอย่างมากด้วยscanfในตอนล่าง แต่ก็ยังช้ากว่า Python สองเท่า

#include <iostream>
#include <time.h>
#include <cstdio>

using namespace std;

int main() {
    char buffer[10000];
    long line_count = 0;
    time_t start = time(NULL);
    int sec;
    int lps;

    int read = 1;
    while(read > 0) {
        read = scanf("%s", buffer);
        line_count++;
    };
    sec = (int) time(NULL) - start;
    line_count--;
    cerr << "Saw " << line_count << " lines in " << sec << " seconds." ;
    if (sec > 0) {
        lps = line_count / sec;
        cerr << "  Crunch speed: " << lps << endl;
    } 
    else
        cerr << endl;
    return 0;
}

ไม่เห็นโพสต์นี้จนกว่าฉันจะทำการแก้ไขครั้งที่สาม แต่ขอขอบคุณอีกครั้งสำหรับคำแนะนำของคุณ น่าแปลกที่ตอนนี้ไม่มีการโจมตีของฉันเทียบกับงูใหญ่เป็นสองเท่าด้วยบรรทัด scanf ใน edit3 ด้านบน ฉันใช้ 2.7 โดยวิธีการ
JJC

10
หลังจากแก้ไขเวอร์ชัน c ++ แล้วเวอร์ชัน stdio นี้จะช้ากว่าเวอร์ชั่น c ++ iostreams บนคอมพิวเตอร์อย่างมาก (3 วินาทีกับ 1 วินาที)
karunski

10

ดีฉันเห็นว่าในการแก้ปัญหาที่สองของคุณคุณเปลี่ยนจากcinไปscanfซึ่งเป็นข้อเสนอแนะที่เป็นครั้งแรกที่ผมจะทำให้คุณ (CIN เป็น sloooooooooooow) ตอนนี้ถ้าคุณเปลี่ยนจากscanfเป็นfgetsคุณจะเห็นอีกประสิทธิภาพที่เพิ่มขึ้น: fgetsเป็นฟังก์ชัน C ++ ที่เร็วที่สุดสำหรับอินพุตสตริง

BTW ไม่รู้เกี่ยวกับสิ่งที่ซิงค์ดี fgetsแต่คุณยังควรพยายาม


2
ยกเว้นfgetsจะผิด (ในแง่ของการนับบรรทัดและในแง่ของการแบ่งบรรทัดข้ามลูปถ้าคุณจำเป็นต้องใช้จริง ๆ ) สำหรับบรรทัดที่มีขนาดใหญ่เพียงพอโดยไม่มีการตรวจสอบเพิ่มเติมสำหรับบรรทัดที่ไม่สมบูรณ์ (และพยายามชดเชยให้เกี่ยวข้องกับการจัดสรรบัฟเฟอร์ขนาดใหญ่ ที่std::getlineจัดการการจัดสรรใหม่เพื่อให้ตรงกับการป้อนข้อมูลจริงได้อย่างราบรื่น) รวดเร็วและผิดนั้นง่าย แต่ก็คุ้มค่าที่จะใช้ "ช้าลงเล็กน้อย แต่ถูกต้อง" ซึ่งการปิดsync_with_stdioทำให้คุณได้รับ
ShadowRanger
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.