memmove()
的代码是高度优化的汇编程序,可能是内联的(没有函数调用开销,尽管对于100KiB的数据,函数调用开销很小)。好处在于更大的移动和简单的循环条件。
Marcus:那么Python也使用 memmove()
还是其他魔法?
- 我还没有查看过Python源码,但可以确定它跟踪其字符串的长度(它们是空终止的,但Python始终知道字符串的活动部分有多长)。知道该长度允许Python使用
memmove()
或 memcpy()
(区别在于 memmove()
即使源和目标重叠也能正常工作; memcpy()
不必在它们重叠时正确工作)。它相对不太可能拥有比 memmove/memcpy
更快的东西。
我修改了C代码,以在我的机器上产生更稳定的时间(Mac OS X 10.7.4,8 GiB 1333 MHz RAM,2.3 GHz Intel Core i7,GCC 4.7.1),并比较 strcpy()
和 strcat()
与 memcpy()
和 memmove()
。请注意,我将循环计数从1000增加到10000以提高定时的稳定性,并且我重复测试所有三种机制10次。可以说,定时循环计数应该再增加5-10倍,以便定时超过一秒钟。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/time.h>
#define L (100*1024)
char s[L+1024];
char c[2*L+1024];
static double time_diff( struct timeval et, struct timeval st )
{
return 1e-6*((et.tv_sec - st.tv_sec)*1000000 + (et.tv_usec - st.tv_usec ));
}
static int foo(void)
{
strcpy(c,s);
strcat(c+L,s);
return 0;
}
static int bar(void)
{
memcpy(c + 0, s, L);
memcpy(c + L, s, L);
return 0;
}
static int baz(void)
{
memmove(c + 0, s, L);
memmove(c + L, s, L);
return 0;
}
static void timer(void)
{
struct timeval st;
struct timeval et;
int i;
memset(s, '1', L);
foo();
gettimeofday(&st,NULL);
for( i = 0 ; i < 10000; i++ )
foo();
gettimeofday(&et,NULL);
printf("foo: %f\n", time_diff(et,st));
gettimeofday(&st,NULL);
for( i = 0 ; i < 10000; i++ )
bar();
gettimeofday(&et,NULL);
printf("bar: %f\n", time_diff(et,st));
gettimeofday(&st,NULL);
for( i = 0 ; i < 10000; i++ )
baz();
gettimeofday(&et,NULL);
printf("baz: %f\n", time_diff(et,st));
}
int main(void)
{
for (int i = 0; i < 10; i++)
timer();
return 0;
}
编译时不会产生任何警告:
gcc -O3 -g -std=c99 -Wall -Wextra -Wmissing-prototypes -Wstrict-prototypes \
-Wold-style-definition cp100k.c -o cp100k
我记录的时间是:
foo: 1.781506
bar: 0.155201
baz: 0.144501
foo: 1.276882
bar: 0.187883
baz: 0.191538
foo: 1.090962
bar: 0.179188
baz: 0.183671
foo: 1.898331
bar: 0.142374
baz: 0.140329
foo: 1.516326
bar: 0.146018
baz: 0.144458
foo: 1.245074
bar: 0.180004
baz: 0.181697
foo: 1.635782
bar: 0.136308
baz: 0.139375
foo: 1.542530
bar: 0.138344
baz: 0.136546
foo: 1.646373
bar: 0.185739
baz: 0.194672
foo: 1.284208
bar: 0.145161
baz: 0.205196
奇怪的是,如果我不使用'no warnings'选项并省略<string.h>
和<stdio.h>
头文件,就像最初发布的代码一样,我得到的时间如下:
foo: 1.432378
bar: 0.123245
baz: 0.120716
foo: 1.149614
bar: 0.186661
baz: 0.204024
foo: 1.529690
bar: 0.104873
baz: 0.105964
foo: 1.356727
bar: 0.150993
baz: 0.135393
foo: 0.945457
bar: 0.173606
baz: 0.170719
foo: 1.768005
bar: 0.136830
baz: 0.124262
foo: 1.457069
bar: 0.130019
baz: 0.126566
foo: 1.084092
bar: 0.173160
baz: 0.189040
foo: 1.742892
bar: 0.120824
baz: 0.124772
foo: 1.465636
bar: 0.136625
baz: 0.139923
眼观这些结果,似乎比“更干净”的代码快,尽管我没有对这两组数据进行学生t检验,并且计时有非常大的变化(但我有像Boinc这样在后台运行8个进程的东西)。当初测试只涉及到strcpy()
和strcat()
函数时,效果似乎更加显著。如果这是一个真正的效应,我无法解释它!
由mvds进行后续跟进:
由于问题已关闭,我无法正常回答。在Mac上不做任何事情时,我得到以下计时结果:(带有标题)foo: 1.694667 bar: 0.300041 baz: 0.301693
foo: 1.696361 bar: 0.305267 baz: 0.298918
foo: 1.708898 bar: 0.299006 baz: 0.299327
foo: 1.696909 bar: 0.299919 baz: 0.300499
foo: 1.696582 bar: 0.300021 baz: 0.299775
(不包含标题,忽略警告)
foo: 1.185880 bar: 0.300287 baz: 0.300483
foo: 1.120522 bar: 0.299585 baz: 0.301144
foo: 1.122017 bar: 0.299476 baz: 0.299724
foo: 1.124904 bar: 0.301635 baz: 0.300230
foo: 1.120719 bar: 0.300118 baz: 0.299673
预处理器输出(-E
标志)显示,包括头文件会将strcpy
翻译成内建调用,例如:
((__builtin_object_size (c, 0) != (size_t) -1) ? __builtin___strcpy_chk (c, s, __builtin_object_size (c, 2 > 1)) : __inline_strcpy_chk (c, s));
((__builtin_object_size (c+(100*1024), 0) != (size_t) -1) ? __builtin___strcat_chk (c+(100*1024), s, __builtin_object_size (c+(100*1024), 2 > 1)) : __inline_strcat_chk (c+(100*1024), s));
所以libc版本的strcpy函数表现优于gcc内置函数。(使用gdb可以轻松验证,如果包含头文件,在strcpy调用上设置断点确实不会中断)
在Linux上(Debian 5.0.9,amd64),差异似乎可以忽略不计。 生成的汇编代码(-S标志)仅在包含的调试信息方面存在差异。
time
命令,它会给出更可靠的结果,而不必在代码中瞎搞。只需编写应用程序所需的代码并使用time
即可。 - Kenmemmove()
或memcpy()
(区别在于memmove()
即使源和目标重叠也可以正常工作;如果它们重叠,memcpy()
不一定能正常工作)。 相对而言,他们很可能没有比memmove / memcpy
更快的东西可用。 - Jonathan Leffler