c - 为什么会从stdin读取行, c 比Python慢

  显示原文与译文双语对照的内容

我想用被比较的字符串从标准输入中读取程序行 python 晶 C++ 还惊奇地看到我的C++ 代码运行比等于把 python 代码要慢一个数量级。 因为我的C++ 是生锈的,而且我还不是专家 Pythonista,请告诉我如果我做错了或者我是否误解了某些东西。


( tl ;dr回答:包括语句: cin.sync_with_stdio(false) 或者只使用 fgets 。

在桌上的我的问题,并期待 ), tl ;dr结果:一直向下滚动到底部


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

编辑: OS-X下我必须说明的是我尝试了这既( 10.6.8 ) 和 Linux 2.6.32 ( RHEL 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:

好的,我尝试了 J.N. 建议让 python 存储行读取: 但这对 python的速度没有任何影响。

我还尝试了 J.N. 建议使用scanf数组代替getline到 std::string 。 答对这导致了 python 和 C++的等价性能。 ( 3,333,333 lp唱片与我的输入数据,即顺便说一下关于 20 char是每次只要短线三个字段组成,通常较宽,虽然有时候更多) 。

代码:


char input_a[512];
char input_b[32];
char input_c[512];
while(scanf("%s %s %sn", 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:: 字符串/getline是典型和合理的。

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

添加:cin.sync_with_stdio(false) ;

在上面的循环中,紧接着的循环会导致运行速度比 python 快的代码。

就某一文件与 20 M 行的text, 新性能比较 ( 这是我的2011 Macbook Pro ),使用的原始代码,本文以带有同步已经禁用,和原 python,分别,. 是的,我运行了几次来消除磁盘缓存错误。


$/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/Better 解决方案:

根据下面的灰色 Gandalf,gets比scanf或者非同步的cin方法更快。 我还了解到,scanf获取因为潜在的缓冲区溢出的都是不安全的,不应使用。 所以,我使用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.");

现在,这里是我们第一次使用的是更大的文件( 100米线;快速恢复具有非常快的磁盘即比较主体上 ~3.4GB) python unsynced cin,而fgets途径,在,并对照传统工作拷贝的实用工具。 [The scanf version segfaulted and i don't feel like troubleshooting it.]:


$/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 vs unsynced cin为字符串是,后者可以读取任意长度的行,而前者需要将输入限定为一些有限的数字 在实践中,这可能是大多数line-based了 non-issue,读输入文件,作为有效的输入缓冲区可以设置为一个非常大的值,该值将无法被超越的。

这是教育。感谢你的评论和建议。

编辑 6:

就像 J.F. Sebastian在下面的评论中所建议的那样,GNU wc工具使用普通的C read() ( 在 safe-read.c 包装中) 来一次读取块( 16字节字节) 并计算新行。 于loop,相关这里是一个 python 等效 J.F. 代码的基础上( 仅显示了相关代码段。用于替代


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 Fragment支撑点,为后两种不存储真正的读取而只是行数的换行。 然而,探索所有不同的实现并考虑性能影响是很有趣的。 再次致谢 !

编辑 7: 微小基准附录和重述摘要

出于完整性考虑,我想我将用原始的( 已经同步) C++ 代码更新同一文件上相同文件的读取速度。 同样,这是一个快速磁盘上的100 M 行文件。 下面是完整的表格:


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同步,这将导致它避免任何输入缓冲。 如果将它的添加到main的顶部,你将看到更好的性能:


cin.sync_with_stdio(false);

通常,当一个输入流被缓冲,而不是每次读取一个字符时,流将以较大的块读取。 这减少了系统调用的数量,这些调用通常比较昂贵。 但是,由于基于FILE*的listobject和iostream通常有单独的实现,因此单独的缓冲区,这可能导致在两者一起使用时出现问题。 例如:


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

如果cin读取的输入多于实际需要的值,那么第个整数值就不能用于scanf函数,它有自己独立的缓冲区。 这会导致意外的结果。

为了避免这种情况,默认情况下,流与href同步。 实现这一点的一种常见方法是让cin根据需要每次读取一个字符,使用的是href函数。 不幸的是,这引入了大量的开销。 对于少量的输入,这不是一个大问题,但是当你阅读数百万行时,性能损失非常显著。

幸运的是,库设计者决定你也可以禁用这项功能,以提高性能,如果你知道自己正在做什么,那么他们提供了sync_with_stdio方法。

我用 G++ 上的在计算机上安装了原始结果。

将以下语句添加到 C++ 版本,恰好在 while 循环使它的与 python 版本内联之前:


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

sync_with_stdio提高了 2秒的速度,并将一个较大的缓冲区设置为 1秒。

在每个test,只是处于好奇我有留意过在幕后发生了什么,而且我已经用 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

yf_terminology_Python@#@#@#python_yf_terminology


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

我可以在我的系统上重现你的结果。 我给两个程序提供了一个 351字节的二进制文件,python 版本除以零,因为它执行太快,C++ 版本需要 12秒。

我取出了平均速度算术并运行了几次测试:

cat 需要平均 0.055 秒( 超过八个运行) 才能将文件转储到 /dev/null

python 版本采用平均 .484 秒和 0.03 ssd ( 超过八个运行) 来计算行。 下面是 /usr/bin/time的一个典型输出,它足以显示( 20800 最大常驻 kb ) 和磁盘 IO ( 0major ==从缓存中读取所有内容) 。


0.48user 0.08system 0:00.56elapsed 98%CPU (0avgtext+0avgdata 20800maxresident)k
0inputs+0outputs (0major+1604minor)pagefaults 0swaps

C++ 版本采用平均 12.32 秒和 0.23 ssd ( 超过八个运行) 来计算行。 /usr/bin/time的一个代表输出只显示 4672 最大常驻 kb,0major 显示所有从缓存读取的内容:


12.34user 0.09system 0:12.45elapsed 99%CPU (0avgtext+0avgdata 4672maxresident)k
0inputs+8outputs (0major+349minor)pagefaults 0swaps

我的空闲内存比我知道的要多:


$ free -m
 total used free shared buffers cached
Mem: 5979 4413 1566 0 226 2594
-/+ buffers/cache: 1591 4387
Swap: 6347 1 6346

作为一个快速的摘要,中的4387free-/+ buffers/cache的折线表示,我有大约4 个g的内存"免费"到内核任何时候它为所欲为。 内存压力不是问题。

python 版本创建了 54898strace -o/tmp/python/tmp/readlines.py </input/file

C++ 版本创建了 89802strace -o/tmp/cpp/tmp/readlines </input/file

顺便说一句,C++ 版本的行计数大于 python 版本的原因之一是,在试图读取超过eof的情况下,eof标志才会被设置。 所以正确的循环是:


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

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

在你的第二个解决方案中,你从 cin 切换到 scanf,这是我将让你成为( cin是 sloooooooooooow )的第一个建议。 在现在 performance,,如果你从 scanf switch 到 gets 中,你将看到另一个 boost: gets 是字符串输入的最快 C++ 函数。

顺便说一下,你不知道同步的事情,很好。 但你还是应该试试 gets

很好的文章。但是我想说的是,scanf的缓冲区溢出问题可以通过指定要读取的字符数来处理。

查看链接中提到的宽度参数。

举例来说,


 char s[10];
 scanf("%9s",s);//This will read at most 9 characters from the input.

 int x;
 scanf("%2d",&x);//This will read a 2 digit number from the input. (just mentioning)

这可以处理缓冲区溢出。 还不能指定动态宽度,但要克服这个问题,只能在 run-time ( 虽然这将防止scanf在编译时进行完整性检查) 处生成格式字符串。

Getline,流 operatoes,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] = '';//make it null-terminated
file.close();

如果需要,你可以在该缓冲区周围环绕一个流,以获得更方便的访问:


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

此外,如果你在控制该文件,请考虑使用一个扁平二进制数据格式而不是文本。 读和写更可靠,因为你不必处理空白的所有歧义。 它也更小,更快解析。

在第二个示例中( 使用 scanf())的原因可能仍然较慢,原因可能是 scanf("%s") 解析字符串并查找任何空间字符( 空格,制表符,换行符) ) 。

而且,是的,CPython做一些缓存,以避免硬盘读操作。

当 C++ 程序读取行时,它必须从磁盘读取文件。 运行 python 程序时,文件已经缓存在内存中。 这可能就是 python 程序运行得更快的原因。

另外,你的C++ 程序将总是计算额外的行,因为你不检查 getline 是否成功,然后再递增计数。 你对 eof的检查既不必要又不正确。

...