[python] 왜 C ++에서 stdin에서 행을 읽는 것이 Python보다 속도가 느립니까?

파이썬과 C ++을 사용하여 stdin에서 문자열 입력 줄을 읽는 것을 비교하고 싶었고 C ++ 코드가 동등한 파이썬 코드보다 느린 속도로 실행되는 것을보고 충격을 받았습니다. 내 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



답변

기본적 cin으로 stdio와 동기화되므로 입력 버퍼링을 피할 수 있습니다. 이것을 메인 상단에 추가하면 성능이 훨씬 향상됩니다.

std::ios_base::sync_with_stdio(false);

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

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

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

이를 피하기 위해 기본적으로 스트림은와 동기화됩니다 stdio. 이를 달성하는 한 가지 일반적인 방법은 함수를 cin사용하여 필요에 따라 각 문자를 한 번에 하나씩 읽는 stdio것입니다. 불행히도 이로 인해 많은 오버 헤드가 발생합니다. 적은 양의 입력의 경우 큰 문제는 아니지만 수백만 줄을 읽을 때는 성능이 저하됩니다.

다행히도 라이브러리 디자이너는 수행중인 작업을 알고있는 경우 성능을 향상시키기 위해이 기능을 비활성화 할 수 있어야하므로 sync_with_stdio방법 을 제공했습니다 .


답변

호기심 때문에 후드에서 어떤 일이 발생하는지 살펴 보았고 각 테스트에서 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


답변

나는 몇 년 뒤에 있지만

원본 게시물의 ‘편집 4/5/6’에서 구성을 사용하고 있습니다.

$ /usr/bin/time cat big_file | program_to_benchmark

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

  1. 실제로 cat벤치 마크가 아닌 의 실행시기를 정하고 있습니다. 표시되는 ‘user’및 ‘sys’CPU 사용량 timecat벤치마킹 된 프로그램이 아니라의 CPU 사용량 입니다. 더 나쁜 것은 ‘실시간’도 반드시 정확하지는 않습니다. cat로컬 OS의 파이프 라인 구현 및 구현에 따라 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

남은 .49 초만 걸렸을 것입니다! 아마 아닙니다 : cat여기 read()에서 파일을 ‘디스크'(실제로는 버퍼 캐시)에서 전송 한 시스템 호출 (또는 동등한)과 파이프가 파일을 전달하기 위해 쓰는 데 대한 비용을 지불 해야했습니다 wc. 올바른 시험은 여전히 ​​그 read()전화 를해야했을 것입니다 . 파이프에 쓰기 및 파이프에서 읽기 호출 만 저장되었을 것이므로 매우 저렴해야합니다.

그래도, 난 당신의 차이를 측정 할 수있을 것이라고 예측 cat file | wc -l하고 wc -l < file그리고 눈에 띄는 (2 자리 비율) 차이를 찾을 수 있습니다. 각각의 느린 테스트는 절대 시간에 비슷한 페널티를 받았습니다. 그러나 전체 시간의 작은 부분에 해당합니다.

실제로 나는 Linux 3.13 (Ubuntu 14.04) 시스템에서 1.5 기가 바이트 가비지 파일로 몇 가지 빠른 테스트를 수행하여 이러한 결과를 얻었습니다 (물론 캐시를 프라이밍 한 후 실제로 ‘최고 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 시간 (사용자 + 시스템)을 사용했다고 주장합니다. 파이프 라인을 인식하는 셸 (bash)의 내장 ‘time’명령을 사용하고 있기 때문입니다. 그리고 파이프 라인의 개별 프로세스가 별도의 코어를 사용하여 CPU 시간을 실시간보다 빠르게 누적 할 수있는 멀티 코어 머신을 사용하고 있습니다. 를 사용하면 /usr/bin/time실시간보다 CPU 시간이 짧아 단일 파이프 라인 요소가 명령 줄에서 전달 된 시간 만 표시 할 수 있음을 보여줍니다. 또한 셸의 출력은 밀리 초를 /usr/bin/time제공 하는 반면 수백 분의 1 초만 제공합니다.

따라서 효율 수준 wc -l은이 cat283분의 409 = 1.453 이상의 실시간 45.3 % = 2.768 및 280분의 775 또는 엄청나게 1백77퍼센트 이상의 CPU를 사용했을 때 큰 차이를 만든다! 내 무작위로 그것은 당시 테스트 상자였습니다.

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

를 실행하면 cat big_file | /usr/bin/time my_program프로그램은 파이프에서 정확한 속도로 전송 된 속도로 입력 한 값 cat을 청크로 수신합니다 cat.

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

물론 두 프로그램간에 동일한 프로그램이 크게 다른 성능을 발휘한다면 흥미로운 벤치 마크 결과입니다. 실제로 프로그램이나 I / O 라이브러리 가를 사용 하는 것과 같이 흥미로운 일을하고 있음을 보여줍니다 mmap(). 실제로 벤치 마크를 두 가지 방법으로 실행하는 것이 좋습니다. 아마도 cat운영 비용을 “용서”하기 위해 작은 요소에 의해 결과를 할인 할 수 있습니다 cat.


답변

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 초로 줄었습니다.


답변

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

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


답변

다음 코드는 지금까지 게시 된 다른 코드보다 더 빠릅니다. ([0, 1000)에서 균일하게 줄 길이를 가진 Visual Studio 2013, 64 비트, 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'; });
}

그것은 모든 파이썬 시도보다 2 배 이상 뛰어납니다.


답변

그런데 C ++ 버전의 행 수가 Python 버전의 수보다 큰 이유는 eof 이상으로 읽으려고 할 때 eof 플래그가 설정되기 때문입니다. 올바른 루프는 다음과 같습니다.

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

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