etc./StackOverFlow

C++에서 Python보다 stdin에서 줄을 읽는 것이 훨씬 느린 이유는 무엇입니까?

청렴결백한 만능 재주꾼 2021. 12. 20. 10:11
반응형

질문자 :Community Wiki


Python과 C++를 사용하여 stdin에서 문자열 입력의 읽기 행을 비교하고 싶었고 내 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++ 코드로 업데이트할 것이라고 생각했습니다. 다시 말하지만, 이것은 고속 디스크의 1억 라인 파일에 대한 것입니다. 다음은 몇 가지 솔루션/접근 방식과의 비교입니다.

구현 초당 라인 수
파이썬(기본값) 3,571,428
cin(기본값/순진한) 819,672
cin(동기화 없음) 12,500,000
fgets 14,285,714
화장실(공정한 비교가 아님) 54,644,808


tl;dr: C++의 기본 설정이 다르기 때문에 더 많은 시스템 호출이 필요합니다.

기본적으로 cin 은 stdio와 동기화되어 입력 버퍼링을 방지합니다. 이것을 메인 상단에 추가하면 훨씬 더 나은 성능을 볼 수 있습니다.

 std::ios_base::sync_with_stdio(false);

일반적으로 입력 스트림이 버퍼링될 때 한 번에 한 문자를 읽는 대신 스트림을 더 큰 청크로 읽습니다. 이것은 일반적으로 상대적으로 비용이 많이 드는 시스템 호출의 수를 줄입니다. 그러나 FILE* 기반 stdioiostreams 종종 별도의 구현을 가지므로 별도의 버퍼가 있기 때문에 둘 다 함께 사용하면 문제가 발생할 수 있습니다. 예를 들어:

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

cin 실제로 필요한 것보다 더 많은 입력을 읽은 경우 자체 독립 버퍼가 scanf 함수에 두 번째 정수 값을 사용할 수 없습니다. 이것은 예기치 않은 결과를 초래할 것입니다.

이를 피하기 위해 기본적으로 스트림은 stdio 와 동기화됩니다. 이를 달성하는 한 가지 일반적인 방법은 stdio cin 각 문자를 한 번에 하나씩 읽도록 하는 것입니다. 불행히도 이것은 많은 오버헤드를 발생시킵니다. 적은 양의 입력에는 큰 문제가 되지 않지만 수백만 줄을 읽을 때는 성능 저하가 상당합니다.

다행히 라이브러리 디자이너는 사용자가 무엇을 하고 있는지 알고 있다면 이 기능을 비활성화하여 성능을 개선할 수 있어야 한다고 결정하여 sync_with_stdio 메서드를 제공했습니다.


Vaughn Cato

호기심에 후드 아래에서 일어나는 일을 살펴보고 각 테스트에서 dtruss/strace를 사용했습니다.

C++

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

시스템 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

시스템 sudo dtruss -c ./a.py < in

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

2mia

나는 여기에서 몇 년 뒤쳐졌지만:

원본 게시물의 'Edit 4/5/6'에서 다음과 같은 구성을 사용하고 있습니다.

 $ /usr/bin/time cat big_file | program_to_benchmark

이것은 몇 가지 다른 방식으로 잘못되었습니다.

  1. 벤치마크가 아니라 cat 의 실행 타이밍을 잡고 있습니다. time 로 표시되는 'user' 및 'sys' CPU 사용량은 벤치마크된 프로그램이 아닌 cat 설상가상으로 '실제' 시간도 반드시 정확하지는 않습니다. cat 및 파이프라인 구현에 따라 cat 이 최종 거대한 버퍼를 작성하고 리더 프로세스가 작업을 완료하기 훨씬 전에 종료될 수 있습니다.

  2. cat 사용하는 것은 불필요하며 사실 역효과입니다. 움직이는 부분을 추가하고 있습니다. 당신이 충분히 오래된 시스템에 있다면(즉, 단일 CPU와 -- 특정 세대의 컴퓨터에서 -- CPU보다 빠른 I/O) -- cat 이 실행되고 있다는 사실만으로도 결과가 상당히 달라질 수 있습니다. 또한 입력 및 출력 버퍼링 및 기타 처리 cat 가 수행할 수 있는 모든 작업의 대상이 됩니다. (내가 Randal Schwartz였다면 이것은 당신에게 '고양이의 쓸모없는 사용' 상을 받을 것입니다.

더 나은 구성은 다음과 같습니다.

 $ /usr/bin/time program_to_benchmark < big_file

이 명령문에서 그것은 big_file을 여는 쉘 이며 이미 열려 있는 파일 설명자로 프로그램에 전달합니다(실제로 time 파일 읽기의 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 나는 몇 가지 이유로 그것들을 언급합니다:

  • POSIX 셸의 I/O 리디렉션 기능에 완전히 익숙하지 않은 사람들에게는 더 '자연스럽습니다'

  • cat 필요한 경우가 있을 수 있습니다(예: 읽을 파일에 액세스하려면 일종의 권한이 필요하고 벤치마킹할 프로그램에 해당 권한을 부여하고 싶지 않습니다. sudo cat /dev/sda | /usr/bin/time my_compression_test --no-output )

  • 실제로 현대 기계 cat 은 아마도 실질적인 결과가 없을 것입니다.

그러나 나는 약간의 망설임과 함께 마지막 말을 한다. 'Edit 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() 호출을 수행해야 했습니다. write-to-pipe 및 read-from-pipe 호출만 저장되었을 것이며 이는 매우 저렴해야 합니다.

그래도 cat file | wc -lwc -l < file 에서 눈에 띄는(2자리 백분율) 차이를 찾습니다. 느린 테스트 각각은 절대 시간에 비슷한 페널티를 지불합니다. 그러나 이는 더 큰 총 시간의 더 작은 부분에 해당합니다.

실제로 Linux 3.13(Ubuntu 14.04) 시스템에서 1.5GB의 쓰레기 파일로 몇 가지 빠른 테스트를 수행하여 다음 결과를 얻었습니다(실제로 캐시를 프라이밍한 후의 '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 시간(user+sys)이 소요되었다고 주장합니다. 이는 파이프라인을 인식하는 셸(bash)의 내장 '시간' 명령을 사용하기 때문입니다. 그리고 저는 파이프라인의 개별 프로세스가 별도의 코어를 사용하여 실시간보다 빠르게 CPU 시간을 축적할 수 있는 멀티 코어 시스템에 있습니다. /usr/bin/time 사용하면 실시간보다 적은 CPU 시간을 볼 수 있습니다. 이는 명령줄에서 전달된 단일 파이프라인 요소만 시간을 측정할 수 있음을 보여줍니다. 또한 쉘의 출력은 밀리초를 제공하는 반면 /usr/bin/time 은 1/100초만 제공합니다.

wc -l 의 효율성 수준에서 cat 는 엄청난 차이를 만듭니다. 409 / 283 = 1.453 또는 45.3% 더 많은 실시간, 775 / 280 = 2.768 또는 무려 177% 더 많은 CPU가 사용됩니다! 내 임의의 그 당시 테스트 상자에 있었습니다.

나는 이러한 테스트 스타일 사이에 적어도 하나의 중요한 다른 차이점이 있다는 점을 추가해야 하며 이것이 이점인지 결함인지 말할 수 없습니다. 이것은 스스로 결정해야 합니다.

cat big_file | /usr/bin/time my_program 을 실행할 때 | cat big_file | /usr/bin/time my_program , 당신의 프로그램에 의해 정확하게 페이스하여 전송에서, 파이프로부터 입력을 수신하지 cat 가 쓴 것보다 더 큰 덩어리와, cat .

/usr/bin/time my_program < big_file 을 실행하면 프로그램은 실제 파일에 대한 열린 파일 설명자를 받습니다. 프로그램 또는 많은 경우 작성된 언어의 I/O 라이브러리는 일반 파일을 참조하는 파일 설명자와 함께 제공될 때 다른 조치를 취할 수 있습니다. read(2) 시스템 호출 mmap(2) 를 사용하여 입력 파일을 주소 공간에 매핑할 수 있습니다. cat 바이너리를 실행하는 데 드는 작은 비용보다 벤치마크 결과에 훨씬 더 큰 영향을 미칠 수 있습니다.

물론 동일한 프로그램이 두 경우 사이에 상당히 다르게 수행된다면 흥미로운 벤치마크 결과입니다. 실제로 프로그램이나 I/O 라이브러리 mmap() 사용과 같이 흥미로운 일을 하고 있음을 보여줍니다. 따라서 실제로는 벤치마크를 양방향으로 실행하는 것이 좋습니다. 아마도 할인 cat 운영 비용 "용서"하는 몇 가지 작은 요인에 의해 결과를 cat 자체를.


Community Wiki

Mac에서 g++를 사용하여 컴퓨터에서 원본 결과를 재현했습니다.

while 루프 직전에 C++ 버전에 다음 명령문을 추가하면 Python 버전과 인라인됩니다.

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

sync_with_stdio 는 속도를 2초로 개선했으며 더 큰 버퍼를 설정하면 1초로 줄어듭니다.


karunski

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);

또한 파일을 제어하는 경우 텍스트 대신 플랫 바이너리 데이터 형식을 사용하는 것이 좋습니다. 공백의 모든 모호성을 처리할 필요가 없기 때문에 읽고 쓰는 것이 더 안정적입니다. 또한 더 작고 구문 분석이 훨씬 빠릅니다.


Stu

다음 코드는 지금까지 여기에 게시된 다른 코드보다 나에게 더 빠릅니다. (Visual Studio 2013, 64비트, [0, 1000]에서 줄 길이가 균일한 500MB 파일).

 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배 이상 능가합니다.


Community Wiki

그건 그렇고, C++ 버전의 줄 수가 Python 버전의 줄 수보다 하나 더 큰 이유는 eof 플래그가 eof 이상으로 읽으려고 할 때만 설정되기 때문입니다. 따라서 올바른 루프는 다음과 같습니다.

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

Gregg

두 번째 예제( scanf() )에서 이것이 여전히 느린 이유는 scanf("%s") 문자열을 구문 분석하고 공백 문자(공백, 탭, 개행)를 찾기 때문일 수 있습니다.

또한 예, CPython은 하드 디스크 읽기를 피하기 위해 일부 캐싱을 수행합니다.


davinchi

글쎄, 나는 당신의 두 번째 솔루션에서 당신이 cin 에서 scanf 로 전환했다는 것을 알았습니다. 이것은 내가 당신을 만들려고 한 첫 번째 제안이었습니다 ( cin 은 sloooooooooooow입니다). 이제 scanf 에서 fgets 전환하면 또 다른 성능 향상을 볼 수 있습니다. fgets 는 문자열 입력을 위한 가장 빠른 C++ 함수입니다.

BTW, 그 동기화에 대해 몰랐습니다. 좋습니다. fgets 시도해야 합니다.


José Ernesto Lara Rodríguez

답변의 첫 번째 요소: <iostream> 은 느립니다. 젠장 천천히. scanf 하면 성능이 크게 향상되지만 여전히 Python보다 2배 느립니다.

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

J.N.

출처 : http:www.stackoverflow.com/questions/9371238/why-is-reading-lines-from-stdin-much-slower-in-c-than-python

반응형