python - لماذا قراءة خطوط من stdin أبطأ بكثير في C++من بايثون؟




benchmarking readline getline (9)

فقط من الفضول لقد dtruss/strace نظرة على ما يحدث تحت غطاء محرك السيارة ، واستخدمت 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

كنت أرغب في مقارنة خطوط القراءة لسلسلة المدخلات من 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). السابق هو ماك بوك برو ، وهذا الأخير هو خادم سمين جدا ، وليس هذا هو وثيقة الصلة للغاية.

تعديل 2: (تمت إزالة هذا التعديل ، لأنه لم يعد ساري المفعول)

$ 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

تحرير 3:

حسنًا ، لقد جربت اقتراح JN بمحاولة امتلاك مخزن Python للخط الذي تمت قراءته: ولكن لم يحدث أي فرق في سرعة بايثون.

لقد حاولت أيضًا اقتراح JN باستخدام scanf في صفيف char بدلاً من getline في std::string . البنغو! هذا أدى إلى أداء مكافئ لكل من Python و C ++. (3333333 LPS مع بيانات المدخلات الخاصة بي ، والتي بالمناسبة هي خطوط قصيرة فقط من ثلاثة حقول لكل منها ، عادة حوالي 20 حرفًا ، مع أنه في بعض الأحيان أكثر).

الشفرة:

char input_a[512];
char input_b[32];
char input_c[512];
while(scanf("%s %s %s\n", input_a, input_b, input_c) != EOF) {
    line_count++;
};

سرعة:

$ cat test_lines | ./readline_test_cpp2
Read 10000000 lines in 3 seconds. LPS: 3333333
$ cat test_lines | ./readline_test2.py
Read 10000000 lines in 3 seconds. LPS: 3333333

(نعم ، ركضت عليه عدة مرات.) لذا ، أعتقد أنني scanf الآن scanf بدلاً من getline . ولكن ، ما زلت أشعر بالفضول إذا اعتقد الناس أن هذا الأداء من std::string / getline هو أمر نموذجي ومعقول.

تحرير 4 (كان: التعديل / الحل النهائي):

مضيفا:

cin.sync_with_stdio(false);

مباشرة فوق بلدي الأصلي أثناء حلقة فوق النتائج في التعليمات البرمجية التي يتم تشغيلها أسرع من بايثون.

مقارنة الأداء الجديدة (هذا على MacBook Pro 2011) ، باستخدام الكود الأصلي والأصل مع تعطيل المزامنة ورمز Python الأصلي ، على التوالي ، في ملف مع 20M سطر من النص. نعم ، ركضت عليه عدة مرات للقضاء على القرص التخزين الخلط.

$ /usr/bin/time cat test_lines_double | ./readline_test_cpp
       33.30 real         0.04 user         0.74 sys
Read 20000001 lines in 33 seconds. LPS: 606060
$ /usr/bin/time cat test_lines_double | ./readline_test_cpp1b
        3.79 real         0.01 user         0.50 sys
Read 20000000 lines in 4 seconds. LPS: 5000000
$ /usr/bin/time cat test_lines_double | ./readline_test.py
        6.88 real         0.01 user         0.38 sys
Read 20000000 lines in 6 seconds. LPS: 3333333

بفضلVaughn Cato لإجابته! يمكن لأي تفصيل يمكن أن يقوم به الأشخاص أو مراجع جيدة أن يشيروا إلى سبب حدوث هذه المزامنة ، وما يعنيه ، وعندما يكون مفيدًا ، وعندما يكون تعطيله أمرًا ممتعًا ، سيكون موضع تقدير كبير بالأجيال القادمة. :-)

تعديل 5 / حل أفضل:

كما اقترح Gandalf The Gray أدناه ، gets أسرع من scanf أو نهج cin غير المتزامن. تعلمت أيضا أن scanf gets على حد سواء UNSAFE ويجب عدم استخدامها بسبب احتمال تجاوز سعة المخزن المؤقت. لذلك ، كتبت هذا التكرار باستخدام fgets ، البديل الأكثر أمانًا. فيما يلي الخطوط ذات الصلة لزملائي noobs:

char input_line[MAX_LINE];
char *result;

//<snip>

while((result = fgets(input_line, MAX_LINE, stdin )) != NULL)
    line_count++;
if (ferror(stdin))
    perror("Error reading stdin.");

الآن ، فيما يلي النتائج باستخدام ملف أكبر (خطوط 100M ؛ ~ 3.4 غيغابايت) على خادم سريع مع قرص سريع للغاية ، مقارنة بين بيثون ، fgets غير المتزامنة ، fgets ، وكذلك مقارنة مع أداة wc . [تجزئة تجزئة إصدار scanf ولا أشعر أن استكشاف الأخطاء وإصلاحها.]:

$ /usr/bin/time cat temp_big_file | readline_test.py
0.03user 2.04system 0:28.06elapsed 7%CPU (0avgtext+0avgdata 2464maxresident)k
0inputs+0outputs (0major+182minor)pagefaults 0swaps
Read 100000000 lines in 28 seconds. LPS: 3571428

$ /usr/bin/time cat temp_big_file | readline_test_unsync_cin
0.03user 1.64system 0:08.10elapsed 20%CPU (0avgtext+0avgdata 2464maxresident)k
0inputs+0outputs (0major+182minor)pagefaults 0swaps
Read 100000000 lines in 8 seconds. LPS: 12500000

$ /usr/bin/time cat temp_big_file | readline_test_fgets
0.00user 0.93system 0:07.01elapsed 13%CPU (0avgtext+0avgdata 2448maxresident)k
0inputs+0outputs (0major+181minor)pagefaults 0swaps
Read 100000000 lines in 7 seconds. LPS: 14285714

$ /usr/bin/time cat temp_big_file | wc -l
0.01user 1.34system 0:01.83elapsed 74%CPU (0avgtext+0avgdata 2464maxresident)k
0inputs+0outputs (0major+182minor)pagefaults 0swaps
100000000


Recap (lines per second):
python:         3,571,428
cin (no sync): 12,500,000
fgets:         14,285,714
wc:            54,644,808

كما ترون ، fgets هو أفضل ، ولكن لا يزال بعيدا جدا عن أداء wc ؛ أنا متأكد من أن هذا يرجع إلى حقيقة أن برنامج wc يفحص كل حرف دون أي نسخ الذاكرة. أظن أنه في هذه المرحلة ، ستصبح أجزاء أخرى من الكود عنق الزجاجة ، لذلك لا أعتقد أن التحسين إلى هذا المستوى سيكون مفيدًا ، حتى إن أمكن ذلك (لأن ، بعد كل شيء ، أحتاج بالفعل إلى تخزين خطوط القراءة في الذاكرة).

لاحظ أيضًا أن المقايضة الصغيرة مع استخدام وحدة fgets char * و fgets مقابل cin غير متزامن إلى سلسلة هي أن الأخيرة يمكن أن تقرأ أي خطوط من أي طول ، بينما تتطلب السابقة إدخال قيود على عدد محدد من الأرقام. من الناحية العملية ، من المحتمل أن يكون هذا غير مشكلة في قراءة معظم ملفات الإدخال القائمة على الخطوط ، حيث يمكن تعيين المخزن المؤقت إلى قيمة كبيرة جدًا لا يمكن تجاوزها بواسطة إدخال صالح.

هذا كان تعليمي. شكرا للجميع على تعليقاتكم واقتراحاتكم.

تحرير 6:

كما اقترح JF Sebastian في التعليقات أدناه ، فإن الأداة المساعدة GNU wc تستخدم read() C واضحة read() (داخل غلاف safe.c المضمن) لقراءة قطع (من 16 كيلو بايت) في وقت واحد وحساب خطوط جديدة. إليك ما يعادل بايثون استنادًا إلى شفرة JF (فقط أظهر القصاصة ذات الصلة التي تحل محل بايثون للحلقة:

BUFFER_SIZE = 16384
count = sum(chunk.count('\n') for chunk in iter(partial(sys.stdin.read, BUFFER_SIZE), ''))

أداء هذا الإصدار سريع للغاية (على الرغم من أنه لا يزال أبطأ قليلاً من أداة C wc الأولية ، بالطبع):

$ /usr/bin/time cat temp_big_file | readline_test3.py
0.01user 1.16system 0:04.74elapsed 24%CPU (0avgtext+0avgdata 2448maxresident)k
0inputs+0outputs (0major+181minor)pagefaults 0swaps
Read 100000000 lines in 4.7275 seconds. LPS: 21152829

مرة أخرى ، من السخف بعض الشيء بالنسبة لي لمقارنة C ++ fgets / cin وأول رمز python من جهة إلى wc -l وآخر مقتطف Python من الجانب الآخر ، حيث أن الأخيرين لا fgets بالفعل خطوط القراءة ، ولكن مجرد الاعتماد على الخطوط الجديدة. لا يزال من المثير للاهتمام استكشاف جميع التطبيقات المختلفة والتفكير في الآثار المترتبة على الأداء. شكرًا لك مرة أخرى!

تحرير 7: إضافة مرجع صغيرة وملخص

للتأكد من اكتمالها ، فكرت في تحديث سرعة القراءة لنفس الملف على نفس المربع مع رمز 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

يمكن أن يكون 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(&buffer[0], length);

أيضًا ، إذا كنت تتحكم في الملف ، ففكر في استخدام تنسيق بيانات ثنائي مسطح بدلاً من النص. إنه أكثر موثوقية للقراءة والكتابة لأنك لست مضطرًا للتعامل مع جميع نقاط الغموض الخاصة بالمسافة البيضاء. انها أيضا أصغر وأسرع بكثير لتحليل.


حسنًا ، أرى أنه في cin الثاني قمت بالتبديل من cin إلى scanf ، وهو أول اقتراح سأقوم به (cin هو sloooooooooooow). الآن ، إذا قمت بالتبديل من scanf إلى fgets ، سترى دفعة أخرى في الأداء: fgets هي أسرع وظيفة C ++ لإدخال السلسلة.

راجع للشغل ، لم أكن أعرف شيئا عن ذلك المزامنة ، لطيفة. ولكن لا يزال عليك محاولة fgets .


العنصر الأول من إجابة: <iostream> بطيء. لعنة بطيئة أحصل على زيادة كبيرة في الأداء مع scanf كما في أدناه ، لكنه لا يزال أبطأ مرتين من بايثون.

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

بالمناسبة ، فإن سبب عد السطر لإصدار C ++ هو واحد أكبر من العدد الخاص بإصدار Python هو أن علامة eof تتحقق فقط عند محاولة القراءة للقراءة خارج نطاق eof. لذا فإن الحلقة الصحيحة ستكون:

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

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

لقد استنسعت النتيجة الأصلية على جهاز الكمبيوتر الخاص بي باستخدام نظام 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 إلى ثانيتين ، كما أدى تعيين مخزن مؤقت أكبر إلى تقليله إلى ثانية واحدة.


بشكل افتراضي ، تتم مزامنة cin بـ stdio ، مما يؤدي إلى تجنب أي تخزين مؤقت للتخزين. إذا أضفت هذا إلى الجزء الرئيسي من الصفحة الرئيسية ، فيجب أن تشاهد أداء أفضل بكثير:

std::ios_base::sync_with_stdio(false);

عادة ، عندما يتم تخزين دفق إدخال مؤقت ، بدلاً من قراءة حرف واحد في كل مرة ، سيتم قراءة الدفق في أجزاء أكبر. هذا يقلل من عدد مكالمات النظام ، والتي عادة ما تكون مكلفة نسبيا. ومع ذلك ، نظرًا لأن stdio و iostreams المستند إلى FILE* غالبًا ما يكون لهما تطبيقات منفصلة وبالتالي مخزونات منفصلة ، فقد يؤدي ذلك إلى حدوث مشكلة إذا تم استخدام كليهما معًا. فمثلا:

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

إذا تمت قراءة المزيد من المدخلات cin من العدد المطلوب فعليًا ، فإن قيمة العدد الصحيح الثاني لن تكون متاحة لوظيفة scanf ، التي تحتوي على مخزن مؤقت مستقل خاص بها. هذا من شأنه أن يؤدي إلى نتائج غير متوقعة.

لتجنب هذا ، بشكل افتراضي ، تتم مزامنة تدفقات مع stdio . إحدى الطرق الشائعة لتحقيق ذلك هي أن يقرأ cin كل حرف واحد في كل مرة حسب الحاجة باستخدام وظائف stdio . لسوء الحظ ، هذا يقدم الكثير من النفقات العامة. بالنسبة إلى كميات صغيرة من المدخلات ، هذه ليست مشكلة كبيرة ، ولكن عندما تقرأ الملايين من الأسطر ، تكون عقوبة الأداء مهمة.

لحسن الحظ ، قرر مصممي المكتبات أنه يجب أيضًا تمكين هذه الميزة للحصول على أداء محسن إذا كنت تعرف ما كنت تفعله ، لذا فقد قدموا طريقة sync_with_stdio .


في المثال الثاني (مع scanf ()) السبب في أن هذا لا يزال أبطأ قد يكون بسبب scanf ("٪ s") يوزع سلسلة ويبحث عن أي حرف مساحة (مسافة ، علامة تبويب ، سطر جديد).

أيضا ، نعم ، CPython يفعل بعض التخزين المؤقت لتجنب القراءة harddisk.


يمكن أن يعمل C / C ++ بشكل أفضل في البرامج حيث يوجد إما صفائف كبيرة أو تكرارًا ثقيلًا على المصفوفات (من أي حجم). هذا هو السبب في أن الرسومات هي أسرع بشكل عام في C / C ++ ، لأن عمليات مجموعة ثقيلة تكمن وراء جميع عمليات الرسومات تقريبا. إن .NET هو بطيء في عمليات فهرسة المصفوفات بسبب كل عمليات التحقق من السلامة ، وهذا ينطبق بشكل خاص على المصفوفات متعددة الأبعاد (ونعم ، صفائف C # مستطيلة حتى أبطأ من صفائف C #).

تكون مكافآت C / C ++ أكثر وضوحًا إذا قمت بالالتصاق مباشرة مع المؤشرات وتجنب Boost و std::vector وغيرها من الحاويات عالية المستوى ، بالإضافة إلى تضمين كل وظيفة صغيرة ممكنة. استخدم صفائف المدرسة القديمة كلما أمكن ذلك. نعم ، ستحتاج إلى مزيد من أسطر التعليمات البرمجية لإنجاز نفس الشيء الذي فعلته في Java أو C # عندما تتجنب الحاويات عالية المستوى. إذا كنت تحتاج إلى مصفوفة ذات حجم ديناميكي ، فستحتاج فقط إلى تذكر إقران new T[] الخاصة بك مع عبارة delete[] المقابلة delete[] (أو استخدام std::unique_ptr ) - سعر السرعة الإضافية هو أنه يجب عليك std::unique_ptr بأكثر دقة . ولكن في المقابل ، تحصل على تخليص نفسك من سعة الذاكرة المدارة / جامع القمامة ، والتي يمكن أن تكون بسهولة 20٪ أو أكثر من وقت التنفيذ للبرامج الموجهة بشكل كبير في Java و .NET ، بالإضافة إلى تلك الضخمة المدارة تكاليف فهرسة مجموعة الذاكرة. يمكن أن تستفيد تطبيقات C ++ أيضًا من بعض مفاتيح التحويل البرمجي الأنيقة في بعض الحالات المحددة.

أنا مبرمج خبير في C و C ++ و Java و C #. لقد أتيحت لي في الآونة الأخيرة فرصة نادرة لتطبيق نفس البرنامج الخوارزمي في اللغات الثلاث الأخيرة. كان البرنامج يحتوي على الكثير من العمليات الحسابية متعددة الأبعاد والرياضيات. أنا بشكل كبير الأمثل هذا في جميع اللغات الثلاث. كانت النتائج نموذجية لما أراه عادة في مقارنات أقل صرامة: كانت لغة جافا أسرع بنحو 1.3 مرة من C # (معظم JVMs هي أكثر ملاءمة من CLR) ، وجاء إصدار المؤشر الخام C ++ في حوالي 2.1x أسرع من C #. لاحظ أن برنامج C # يستخدم رمزًا آمنًا فقط - إنه رأيي أنك قد تقوم أيضًا برمجته في C ++ قبل استخدام الكلمة الرئيسية unsafe .

لئلا يعتقد أي شخص لدي شيء ضد C # ، سأغلق عن طريق القول أن C # ربما لغتي المفضلة. إنها لغة التطوير الأكثر منطقية وبديهية وسريعة التي واجهتها حتى الآن. أفعل كل النماذج الخاصة بي في C #. لغة C # لديها العديد من المزايا الصغيرة والرائعة على Java (نعم ، أعرف أن مايكروسوفت لديها فرصة لإصلاح العديد من أوجه القصور في Java عن طريق دخول اللعبة في وقت متأخر ونسخ نسخة جافا). نخب لفصول Calendar جافا أي شخص؟ إذا أنفقت مايكروسوفت جهدًا حقيقيًا لتحسين CLR و. NET JITter ، فإن C # يمكن أن تتولى الأمر بجدية. أنا متفاجئ بصدق أنهم لم يفعلوا ذلك بالفعل - لقد فعلوا الكثير من الأمور في لغة C # ، فلماذا لا نتابعها باستخدام تحسينات مترجمات قوية؟ ربما لو كنا جميعا التسول.







c++ python benchmarking readline getline