为什么阅读stdin中的代码比C ++慢得多?

我想比较使用Python和C ++的stdinstringinput的读取行,并且震惊地看到我的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 

Python等效:

 #!/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(雪豹)和Linux 2.6.32(红帽子Linux 6.2)下都试过。 前者是一台MacBook Pro,而后者是一台非常强大的服务器,并不是说这样太贴切了。

编辑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的build议,试图让Python存储行读取:但它没有区别于python的速度。

我也试过JN的build议,使用scanfchar数组而不是getlinestd::string 。 答对了! 这导致Python和C ++的性能相当。 (我的input数据是3,333,333个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而不是getline 。 但是,如果人们认为std::string / getline是典型的和合理的,我还是很好奇。

编辑4(是:最终编辑/解决scheme):

添加:

 cin.sync_with_stdio(false); 

紧接在上面我原来的while循环之上的结果是运行得比Python快的代码。

新的性能比较 (这是在我的2011年MacBook Pro上),使用原始代码,原始禁用同步,和原始的Python代码,分别在一个文本20M行文本。 是的,我跑了几次,以消除磁盘caching混淆。

 $ /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 /更好的解决scheme:

正如下面的Gandalf The Grey所build议的,比scanf或者非同步的cin方法更快。 我也了解到scanfgets都是不安全的,不应该被使用,因为可能存在缓冲区溢出。 所以,我用fgets写了这个迭代,这是获得更安全的select。 以下是我的同伴小老鼠的相关路线:

 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 GB)的结果,比较Python代码,非同步cinfgets方法,以及比较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检查每个字符而没有任何内存复制的事实。 我怀疑,在这一点上,代码的其他部分将成为瓶颈,所以我不认为优化到这个水平甚至是可能的(因为毕竟,我实际上需要存储读取线在记忆中)。

还要注意,使用char *缓冲区和fgets与非同步cin进行string的小折衷是,后者可以读取任意长度的行,而前者需要将input限制为某个有限数字。 在实践中,读取大多数基于行的input文件可能不是问题,因为缓冲区可以设置为非常大的值,而不会被有效input超出。

这已经是教育。 感谢大家的意见和build议。

编辑6:

正如JF Sebastian在下面的注释中所build议的那样,GNU wc实用程序使用plain C read() (在safe-read.c包装内)来一次读取16k字节块并计算新行。 下面是一个基于JF代码的Python等价物(只显示了替代Python for循环的相关代码片段:

 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代码片段进行比较是有点愚蠢的,因为后两者实际上并不存储读取的行,但是只需要换行。 不过,探索所有不同的实现并考虑性能影响是很有趣的。 再次感谢!

编辑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 

默认情况下, cin与stdio同步,这使得它避免了任何input缓冲。 如果你把它添加到你的主体的顶部,你应该看到更好的性能:

 std::ios_base::sync_with_stdio(false); 

通常,当inputstream被缓冲时,而不是一次读取一个字符,stream将被以较大的块读取。 这减less了系统调用的数量,这通常相对昂贵。 但是,由于基于FILE*stdioiostreams通常具有不同的实现方式,因此具有不同的缓冲区,如果两者一起使用,则可能会导致问题。 例如:

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

如果cin读取的input比实际需要的多,则第二个整数值将不可用于scanf函数,该函数具有自己的独立缓冲区。 这会导致意想不到的结果。

为了避免这种情况,默认情况下,stream与stdio同步。 一个常见的方法是让cin根据需要使用stdio函数逐个读取每个字符。 不幸的是,这引起了很多开销。 对于less量的input,这不是一个大问题,但是当你阅读数百万行代码时,性能损失是很大的。

幸运的是,如果你知道自己在做什么,库devise者决定你也应该禁用这个function来提高性能,所以他们提供了sync_with_stdio方法。

出于好奇,我已经看了一下发生了什么,我在每个testing中都使用了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 

python

 ./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 

我在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秒。

我在这里落后了几年,但是:

在原始文章的“编辑4/5/6”中,您使用的是构造:

 $ /usr/bin/time cat big_file | program_to_benchmark 

这在几个不同的方面是错误的:

  1. 你实际上是在计算`cat`的执行时间,而不是你的基准。 `time'显示的'user'和'sys'CPU使用率是`cat`的,而不是基准程序。 更糟的是,“真实”的时间也不一定准确。 根据本地操作系统中“cat”和pipe道的实现情况,“cat”可能会写入一个最终的巨大缓冲区,并在读者进程完成之前退出。

  2. 使用“猫”是不必要的,事实上适得其反。 你正在添加移动部件。 如果你使用的是一个足够老的系统(例如,使用一个CPU,并且在某些代计算机中,I / O速度比CPU更快),那么“cat”运行的事实可能会大大增加结果的颜色。 你也受到任何input和输出缓冲以及`cat`可能做的其他处理。 (如果我是Randal Schwartz,这可能会使你获得“无用的猫”奖: https ://en.wikipedia.org/wiki/Cat_(Unix)#Useless_use_of_cat)

更好的build设将是:

 $ /usr/bin/time program_to_benchmark < big_file 

在这个声明中,它是打开big_file的shell ,将它传递给你的程序(实际上是作为一个subprocess执行你的程序的“时间”),作为一个已经打开的文件描述符。 100%的文件阅读是严格的程序的责任,你正在尝试基准。 这可以让您真正了解其性能,而不会产生虚假的复杂情况。

我会提到两个可能的,但实际上是错误的,也可以考虑的“修复”(但是我对它们进行了“不同的编号”,因为这些并不是原始文章中的错误):

答:您可以通过只计算您的程序来“解决”这个问题:

 $ cat big_file | /usr/bin/time program_to_benchmark 

B.或通过对整个pipe道进行计时:

 $ /usr/bin/time sh -c 'cat big_file | program_to_benchmark' 

由于与#2相同的原因,这些都是错误的:他们仍在不必要地使用“猫”。 我提到他们有几个原因:

  • 对于那些不熟悉POSIX shell的I / Oredirectfunction的人来说,它们更“自然”

  • 可能有些情况需要`cat`(例如:要读取的文件需要某种权限才能访问,而且您不希望将该特权授予要进行基准testing的程序:`sudo cat / dev / sda | / usr / bin / time my_compression_test –no-output`)

  • 在实践中 ,在现代化的机器上,pipe道中增加的“猫”可能没有真正的结果

但是我有些犹豫地说最后一件事。 如果我们查看“编辑5”中的最后一个结果 –

 $ /usr/bin/time cat temp_big_file | wc -l 0.01user 1.34system 0:01.83elapsed 74%CPU ... 

– 这声称在testing过程中`cat`消耗了74%的CPU; 事实上1.34 / 1.83约为74%。 也许是一个运行:

 $ /usr/bin/time wc -l < temp_big_file 

本来只需要剩下的49秒! 可能不是:这里的`cat`不得不支付read()系统调用(或者等价的),这个调用从'disk'(实际上是缓冲区caching)传输文件,pipe道写入将它们传递给`wc`。 正确的testing将不得不做read()调用; 只有写入pipe道和读取pipe道的调用才能被保存,而且这些应该相当便宜。

不过,我预测你可以测量`cat file |之间的差异 wc -l`和`wc -l <​​file`,并find明显的(2位数百分比)差异。 每个较慢的testing都会在绝对时间内支付类似的惩罚; 这将占其较大总时间的一小部分。

事实上,我在Linux 3.13(Ubuntu 14.04)系统上做了一些1.5 GB的垃圾文件的快速testing,获得了这些结果(当然这些结果实际上是'3中最好的结果;在启动caching之后):

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

请注意,这两个pipe道结果声称占用了比实时更多的CPU时间(用户+ sys)。 这是因为我正在使用shell(Bash)的内置“时间”命令,这是对pipe道的认识; 而且我在一台多核机器上,stream水线中的单独进程可以使用不同的内核,所以CPU时间比实时更快。 使用/ usr / bin / time我看到的CPU时间比实时更短 – 表明它只能对在命令行上传递给它的单个pipe道元素计时。 另外,shell的输出会给出毫秒,而/ usr / bin / time只能给出秒数。

因此,在`wc -l`的效率水平上,`cat`会产生巨大的差异:409/283 = 1.453或45.3%的实时性,775/280 = 2.768,或者高达177%的CPU使用率! 在我的随机它是在那里的时间testing框。

我还要补充说,这些testing方式之间至less还有另外一个重要的区别,我不能说这是一种好处还是错误; 你必须自己决定:

当你运行`cat big_file | / usr / bin / time my_program`,你的程序正在接收来自pipe道的input,正好以`cat`发送的速度,并且不大于`cat`所写的大小。

当你运行`/ usr / bin / time my_program <big_file`时,你的程序收到一个打开的文件描述符到实际的文件。 您的程序 – 或者在很多情况下,写入语言的I / O库 – 在提供引用常规文件的文件描述符时可能采取不同的操作。 它可以使用mmap(2)将input文件映射到其地址空间,而不是使用显式读(2)系统调用。 这些差异对基准testing结果的影响要比运行`cat`二进制文件的小成本要大得多。

当然这是一个有趣的基准testing结果,如果同一个程序在两种情况下performance的差别很大。 这表明,程序或其I / O库的确正在做一些有趣的事情,比如使用mmap()。 所以在实践中,两种方式都可以运行基准。 或许以一些小的因素来折扣“猫”结果,以“原谅”运行“猫”本身的成本。

如果你不关心文件加载时间,或者如果你正在加载小文本文件,Getline,stream操作符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(); 

如果你愿意,你可以在这个缓冲区中包装一个stream,以便更方便地访问,如下所示:

 std::istrstream header(&buffer[0], length); 

另外,如果您在控制文件,请考虑使用平面二进制数据格式而不是文本。 读取和写入更可靠,因为您不必处理空白的所有含糊之处。 分析也更小,速度更快。

顺便说一下,C ++版本的行数大于Python版本的数量的原因在于,eof标志只有在尝试读取超出eof时才被设置。 所以正确的循环将是:

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

在你的第二个例子(与scanf())之间,为什么这仍然较慢的原因可能是因为scanf(“%s”)分析string,并寻找任何空间字符(空格,制表符,新行)。

另外,是的,CPython会做一些caching来避免硬盘读取。

答案的第一个元素: <iostream>很慢。 该死的慢。 如下所示,我使用scanf获得了巨大的性能提升,但仍然比Python慢​​两倍。

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

以下代码对于我来说比迄今为止发布的其他代码更快:(Visual Studio 2013,64位,500 MB文件,行长度均匀地在[0,1000)中)。

 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。

那么,我看到在你的第二个解决scheme中,你从cin切换到scanf ,这是我要给你的第一个build议(cin is sloooooooooooow)。 现在,如果你从scanf切换到fgets ,你会看到性能上的另一个提升: fgets是stringinput最快的C ++函数。

顺便说一句,不知道有关同步的事情,很好。 但是你还是应该试试fgets