질문자 :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*
기반 stdio
및 iostreams
종종 별도의 구현을 가지므로 별도의 버퍼가 있기 때문에 둘 다 함께 사용하면 문제가 발생할 수 있습니다. 예를 들어:
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
이것은 몇 가지 다른 방식으로 잘못되었습니다.
벤치마크가 아니라 cat
의 실행 타이밍을 잡고 있습니다. time
로 표시되는 'user' 및 'sys' CPU 사용량은 벤치마크된 프로그램이 아닌 cat
설상가상으로 '실제' 시간도 반드시 정확하지는 않습니다. cat
및 파이프라인 구현에 따라 cat
이 최종 거대한 버퍼를 작성하고 리더 프로세스가 작업을 완료하기 훨씬 전에 종료될 수 있습니다.
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 -l
및 wc -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 WikiMac에서 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초로 줄어듭니다.
karunskigetline
, 스트림 연산자 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