ความแตกต่างอย่างมาก (x9) ในเวลาดำเนินการระหว่างโค้ดที่เหมือนกันเกือบทั้งหมดใน C และ C ++


85

ฉันพยายามแก้แบบฝึกหัดนี้จาก www.spoj.com: FCTRL - Factorial

คุณไม่จำเป็นต้องอ่านจริงๆแค่ทำถ้าคุณอยากรู้ :)

ก่อนอื่นฉันติดตั้งในC ++ (นี่คือวิธีแก้ปัญหาของฉัน):

#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;
}

ฉันอัปโหลดเป็นโซลูชันสำหรับg ++ 5.1

ผลลัพธ์ที่ได้คือ: เวลา 0.18 Mem 3.3M ผลการดำเนินการ C ++

แต่แล้วฉันก็เห็นความคิดเห็นบางส่วนที่อ้างว่าเวลาดำเนินการน้อยกว่า 0.1 เนื่องจากฉันไม่สามารถคิดถึงอัลกอริทึมที่เร็วขึ้นฉันจึงพยายามใช้รหัสเดียวกันใน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;
}

ฉันอัปโหลดเป็นโซลูชันสำหรับgcc 5.1

ครั้งนี้ผลลัพธ์คือ: เวลา 0.02 Mem 2.1M ผลการดำเนินการ C

ตอนนี้รหัสเกือบจะเหมือนกันฉันได้เพิ่มstd::ios_base::sync_with_stdio(false);รหัส C ++ ตามที่แนะนำไว้ที่นี่เพื่อปิดการซิงโครไนซ์กับบัฟเฟอร์ stdio ของไลบรารี C ฉันยังแยกprintf("%d\n", num_of_trailing_zeros);ไปprintf("%d", num_of_trailing_zeros); printf("%s","\n");เพื่อชดเชยสำหรับการโทรคู่ในoperator<<cout << num_of_trailing_zeros << "\n";

แต่ฉันยังเห็นประสิทธิภาพที่ดีขึ้น x9และการใช้หน่วยความจำลดลงในรหัส C เทียบกับรหัส C ++

ทำไมถึงเป็นเช่นนั้น?

แก้ไข

ฉันคงunsigned longไปunsigned intในรหัสซี ควรจะเป็นunsigned intและผลลัพธ์ที่แสดงไว้ด้านบนเกี่ยวข้องกับunsigned intเวอร์ชันใหม่ ( )


31
สตรีม C ++ ช้ามากจากการออกแบบ เพราะช้าและมั่นคงชนะการแข่งขัน : P ( วิ่งก่อนที่ฉันจะลุกเป็นไฟ )
ลึกลับ

7
ความช้าไม่ได้มาจากความปลอดภัยหรือความสามารถในการปรับตัว เป็นวิธีที่ได้รับการออกแบบด้วยแฟล็กสตรีมทั้งหมด
Karoly Horvath

8
@AlexLop. การใช้ a std::ostringstreamเพื่อสะสมเอาต์พุตและส่งไปยังstd::cout ทั้งหมดในครั้งเดียวในตอนท้ายจะทำให้เวลาลดลงเหลือ 0.02 การใช้std::coutในวงจะช้ากว่าในสภาพแวดล้อมและฉันไม่คิดว่าจะมีวิธีง่ายๆในการปรับปรุง
Blastfurnace

6
ไม่มีใครเกี่ยวข้องกับความจริงที่ว่าการกำหนดเวลาเหล่านี้ได้มาโดยใช้ ideone หรือไม่?
ildjarn

6
@Olaf: ฉันกลัวว่าฉันไม่เห็นด้วยคำถามประเภทนี้มีมากในหัวข้อสำหรับแท็กที่เลือกทั้งหมด C และ C ++ นั้นใกล้เคียงกันมากพอโดยทั่วไปแล้วความแตกต่างในประสิทธิภาพดังกล่าวขอคำอธิบาย ฉันดีใจที่เราพบมัน บางที GNU libc ++ ควรได้รับการปรับปรุงด้วยเหตุนี้
chqrlie

คำตอบ:


56

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

การสแกนอินพุตด้วยscanf("%d", &fact_num);ด้านหนึ่งและอีกด้านหนึ่งcin >> fact_num;ดูเหมือนจะไม่เสียค่าใช้จ่ายมากนัก ในความเป็นจริงควรมีค่าใช้จ่ายน้อยกว่าใน C ++ เนื่องจากชนิดของการแปลงเป็นที่รู้จักในเวลาคอมไพล์และตัวแยกวิเคราะห์ที่ถูกต้องสามารถเรียกใช้โดยตรงโดยคอมไพเลอร์ C ++ เช่นเดียวกันสำหรับเอาต์พุต คุณยังทำให้จุดของการเขียนการโทรที่แยกต่างหากสำหรับprintf("%s","\n");แต่เรียบเรียง C putchar('\n');คือที่ดีพอที่จะรวบรวมนี้เป็นสายไป

ดังนั้นเมื่อพิจารณาถึงความซับซ้อนของทั้ง I / O และการคำนวณเวอร์ชัน C ++ ควรเร็วกว่าเวอร์ชัน C

การปิดใช้งานการบัฟเฟอร์ของstdoutการใช้งาน C ที่ช้าลงโดยสิ้นเชิงกับสิ่งที่ช้ากว่าเวอร์ชัน C ++ การทดสอบอื่นโดย AlexLop พร้อมกับfflush(stdout);after the last printfให้ประสิทธิภาพใกล้เคียงกับเวอร์ชัน C ++ ไม่ช้าเท่ากับการปิดใช้งานบัฟเฟอร์อย่างสมบูรณ์เนื่องจากเอาต์พุตถูกเขียนลงในระบบเป็นชิ้นเล็ก ๆ แทนที่จะเป็นทีละไบต์

เรื่องนี้ดูเหมือนจะชี้ไปที่พฤติกรรมที่เฉพาะเจาะจงในห้องสมุดของคุณ c ++ A: ฉันสงสัยว่าการดำเนินงานของระบบของcinและcoutวูบวาบออกไปเมื่อมีการร้องขอการป้อนข้อมูลจากcout cinไลบรารี C บางตัวก็ทำเช่นนี้เช่นกัน แต่โดยปกติแล้วก็ต่อเมื่ออ่าน / เขียนเข้าและออกจากเทอร์มินัลเท่านั้น การเปรียบเทียบโดยเว็บไซต์ www.spoj.com อาจเปลี่ยนเส้นทางอินพุตและเอาต์พุตไปยังและจากไฟล์

AlexLop ได้ทำการทดสอบอีกครั้ง: การอ่านอินพุตทั้งหมดพร้อมกันในเวกเตอร์จากนั้นการคำนวณและการเขียนผลลัพธ์ทั้งหมดจะช่วยให้เข้าใจว่าเหตุใดเวอร์ชัน C ++ จึงช้ากว่ามาก เพิ่มประสิทธิภาพให้กับเวอร์ชัน C ซึ่งพิสูจน์ประเด็นของฉันและขจัดความสงสัยเกี่ยวกับรหัสการจัดรูปแบบ C ++

การทดสอบอีกครั้งโดย Blastfurnace การจัดเก็บเอาต์พุตทั้งหมดในstd::ostringstreamและล้างซึ่งในการระเบิดครั้งเดียวในตอนท้ายจะช่วยปรับปรุงประสิทธิภาพ C ++ ให้เป็นเวอร์ชัน C พื้นฐาน QED

การอินเทอร์เลซอินพุตจากcinและเอาต์พุตcoutดูเหมือนจะทำให้การจัดการ I / O ที่ไม่มีประสิทธิภาพมากเอาชนะโครงร่างบัฟเฟอร์สตรีม ลดประสิทธิภาพการทำงานเป็น 10

PS: อัลกอริทึมของคุณไม่ถูกต้องสำหรับfact_num >= UINT_MAX / 5เพราะจะล้นและห่อรอบก่อนที่มันจะกลายเป็นfives *= 5 > fact_numคุณสามารถแก้ไขปัญหานี้โดยการหรือถ้าหนึ่งในประเภทนี้มีขนาดใหญ่กว่า ยังใช้เป็นรูปแบบ คุณโชคดีที่พวกที่ www.spoj.com ไม่เข้มงวดกับเกณฑ์มาตรฐานของพวกเขามากเกินไปfivesunsigned longunsigned long longunsigned int%uscanf

แก้ไข: ตามที่ vitaux อธิบายในภายหลังพฤติกรรมนี้ได้รับคำสั่งจากมาตรฐาน C ++ cinจะเชื่อมโยงกับcoutค่าเริ่มต้น การดำเนินcinการป้อนข้อมูลที่บัฟเฟอร์อินพุตต้องการการเติมจะทำให้coutล้างเอาต์พุตที่รอดำเนินการ ในการใช้งานของ OP cinดูเหมือนว่าจะล้างออกcoutอย่างเป็นระบบซึ่งค่อนข้างมากเกินไปและไม่มีประสิทธิภาพอย่างเห็นได้ชัด

Ilya Popov ให้วิธีง่ายๆสำหรับสิ่งนี้: cinสามารถแก้ได้coutโดยการร่ายเวทย์มนตร์อื่นนอกเหนือจากstd::ios_base::sync_with_stdio(false);:

cin.tie(nullptr);

นอกจากนี้ทราบว่าล้างบังคับดังกล่าวเกิดขึ้นเมื่อใช้std::endlแทนในการผลิตปลายสายบน'\n' coutการเปลี่ยนบรรทัดเอาต์พุตเป็นสำนวน C ++ ที่มากขึ้นและดูไร้เดียงสาcout << num_of_trailing_zeros << endl;จะทำให้ประสิทธิภาพลดลงในลักษณะเดียวกัน


2
คุณอาจพูดถูกเกี่ยวกับการสตรีมฟลัชชิง การรวบรวมเอาต์พุตใน a std::ostringstreamและการส่งออกทั้งหมดครั้งเดียวในตอนท้ายจะทำให้เวลาลดลงเท่ากับเวอร์ชัน C
Blastfurnace

2
@ DavidC.Rankin: ฉันพยายามคาดเดา (cout ถูกล้างเมื่ออ่าน cin) คิดหาวิธีพิสูจน์ AlexLop นำไปใช้และให้หลักฐานที่น่าเชื่อถือ แต่ Blastfurnace มีวิธีอื่นในการพิสูจน์ประเด็นของฉันและการทดสอบของเขา ให้หลักฐานที่น่าเชื่อถืออย่างเท่าเทียมกัน ฉันใช้มันเพื่อพิสูจน์ แต่แน่นอนว่ามันไม่ใช่หลักฐานที่เป็นทางการอย่างสมบูรณ์โดยดูที่ซอร์สโค้ด C ++ สามารถทำได้
chqrlie

2
ฉันลองใช้ostringstreamสำหรับเอาต์พุตและให้เวลา 0.02 QED :) เกี่ยวกับfact_num >= UINT_MAX / 5จุดที่ดี!
อเล็กซ์ลอป

1
การรวบรวมอินพุตทั้งหมดลงใน a vectorแล้วประมวลผลการคำนวณ (โดยไม่ostringstream) ให้ผลลัพธ์เหมือนกัน! เวลา 0.02. การรวมทั้งสองอย่างเข้าด้วยกันvectorและostringstreamไม่ได้ปรับปรุงให้ดีขึ้น เวลาเดียวกัน0.02
Alex Lop

2
การแก้ไขที่ง่ายกว่าซึ่งใช้งานได้แม้ว่าsizeof(int) == sizeof(long long)จะเป็นเช่นนี้: เพิ่มการทดสอบในเนื้อหาของลูปหลังจากnum_of_trailing_zeros += fact_num/fives;เพื่อตรวจสอบว่าfivesถึงจุดสูงสุดแล้วหรือไม่:if (fives > UINT_MAX / 5) break;
chqrlie

44

เคล็ดลับอีกอย่างที่จะทำให้iostreamเร็วขึ้นเมื่อคุณใช้ทั้งสองอย่างcinและcoutคือการโทร

cin.tie(nullptr);

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

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

ในกรณีนี้คุณต้องการตรวจสอบให้แน่ใจว่าได้แสดงพร้อมต์จริงก่อนที่คุณจะเริ่มรอการป้อนข้อมูล ด้วยเส้นด้านบนคุณจะทำลายเน็คไทนั้นcinและcoutเป็นอิสระ

ตั้งแต่ C ++ 11 อีกวิธีหนึ่งในการบรรลุประสิทธิภาพที่ดีขึ้นกับ iostreams คือการใช้std::getlineร่วมกับstd::stoiสิ่งนี้:

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

วิธีนี้อาจใกล้เคียงกับสไตล์ C หรือเหนือกว่าscanfด้วยซ้ำ การใช้getcharและโดยเฉพาะอย่างยิ่งgetchar_unlockedร่วมกับการแยกวิเคราะห์ด้วยลายมือยังคงให้ประสิทธิภาพที่ดีกว่า

ปล. ฉันได้เขียนโพสต์เปรียบเทียบหลายวิธีในการป้อนตัวเลขใน C ++ ซึ่งมีประโยชน์สำหรับผู้ตัดสินออนไลน์ แต่เป็นภาษารัสเซียเท่านั้นขออภัย อย่างไรก็ตามควรเข้าใจตัวอย่างโค้ดและตารางสุดท้าย


1
ขอบคุณสำหรับคำอธิบายและ +1 สำหรับวิธีแก้ปัญหา แต่ทางเลือกที่เสนอของคุณมีstd::readlineและstd::stoiไม่สามารถใช้งานได้เทียบเท่ากับรหัส OPs ทั้งสองcin >> x;และscanf("%f", &x);ยอมรับช่องว่างมดเป็นตัวคั่นอาจมีตัวเลขหลายตัวในบรรทัดเดียวกัน
chqrlie

27

ปัญหาคือการอ้างcppreference :

อินพุตใด ๆ จาก std :: cin เอาต์พุตไปยัง std :: cerr หรือการสิ้นสุดโปรแกรมบังคับให้เรียกไปที่ std :: cout.flush ()

นี่เป็นเรื่องง่ายที่จะทดสอบ: ถ้าคุณเปลี่ยน

cin >> fact_num;

ด้วย

scanf("%d", &fact_num);

และเหมือนกันcin >> num_of_inputsแต่ให้coutคุณได้รับประสิทธิภาพเดียวกันในเวอร์ชัน C ++ ของคุณ (หรือเวอร์ชัน IOStream) เช่นเดียวกับใน C หนึ่ง:

ใส่คำอธิบายภาพที่นี่

สิ่งเดียวกันนี้จะเกิดขึ้นหากคุณเก็บไว้cinแต่แทนที่

cout << num_of_trailing_zeros << "\n";

ด้วย

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

วิธีแก้ปัญหาง่ายๆคือการแก้coutและcinตามที่ Ilya Popov กล่าวไว้:

cin.tie(nullptr);

การใช้งานไลบรารีมาตรฐานได้รับอนุญาตให้ละเว้นการเรียกให้ล้างในบางกรณี แต่ไม่เสมอไป นี่คือคำพูดจาก C ++ 14 27.7.2.1.3 (ขอบคุณ chqrlie):

คลาส basic_istream :: sentry: อันดับแรกถ้า is.tie () ไม่ใช่ตัวชี้ค่าว่างฟังก์ชันจะเรียกใช้ is.tie () -> flush () เพื่อซิงโครไนซ์ลำดับเอาต์พุตกับสตรีม C ภายนอกที่เกี่ยวข้อง ยกเว้นว่าสามารถระงับการเรียกนี้ได้หากพื้นที่ใส่ของ is.tie () ว่างเปล่า การใช้งานเพิ่มเติมได้รับอนุญาตให้เลื่อนการเรียกเพื่อล้างจนกว่าจะมีการเรียก is.rdbuf () -> underflow () เกิดขึ้น หากไม่มีการเรียกดังกล่าวเกิดขึ้นก่อนที่วัตถุยามจะถูกทำลายการเรียกเพื่อล้างอาจถูกตัดออกทั้งหมด


ขอบคุณสำหรับคำอธิบาย ยังอ้างถึง C ++ 14 27.7.2.1.3: Class basic_istream :: sentry : อันดับแรกถ้าis.tie()ไม่ใช่ตัวชี้ค่าว่างฟังก์ชันis.tie()->flush()จะเรียกให้ซิงโครไนซ์ลำดับเอาต์พุตกับสตรีม C ภายนอกที่เกี่ยวข้อง ยกเว้นว่าสามารถระงับการเรียกนี้ได้หากพื้นที่is.tie()ว่างเปล่า การใช้งานเพิ่มเติมได้รับอนุญาตให้เลื่อนการโทรออกไปจนกว่าจะมีการโทรis.rdbuf()->underflow()เกิดขึ้น หากไม่มีการเรียกดังกล่าวเกิดขึ้นก่อนที่วัตถุยามจะถูกทำลายการเรียกเพื่อล้างอาจถูกตัดออกทั้งหมด
chqrlie

ตามปกติของ C ++ สิ่งต่างๆจะซับซ้อนกว่าที่คิด ไลบรารี C ++ ของ OP ไม่มีประสิทธิภาพเท่าที่มาตรฐานอนุญาต
chqrlie

ขอบคุณสำหรับลิงค์ cppreference ฉันไม่ชอบ "คำตอบที่ผิด" ในหน้าจอการพิมพ์แม้ว่า☺
Alex Lop

@AlexLop. อ๊ะแก้ไขปัญหา "คำตอบผิด" =) ลืมอัปเดต cin อื่น ๆ (สิ่งนี้ไม่มีผลต่อเวลา)
vitaut

@chqrlie ถูกต้อง แต่แม้ในกรณีที่มีประสิทธิภาพต่ำก็มีแนวโน้มที่จะแย่ลงเมื่อเทียบกับโซลูชัน stdio ขอบคุณสำหรับการอ้างอิงมาตรฐาน
vitaut
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.