簡體   English   中英

連接兩個字符串時,為什么Python比C更快?

[英]Why is Python faster than C when concatenating two strings?

目前我想比較Python和C的速度,當它們用於做字符串時。 我認為C應該提供比Python更好的性能; 但是,我得到了相反的結果。

這是C程序:

#include <unistd.h>
#include <sys/time.h>

#define L (100*1024)

char s[L+1024];
char c[2*L+1024];

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

int foo()
{
    strcpy(c,s);
    strcat(c+L,s);
    return 0;
}

int main()
{
    struct timeval st;
    struct timeval et;
    int i;
    //printf("s:%x\nc:%x\n", s,c);

    //printf("s=%d c=%d\n", strlen(s), strlen(c));
    memset(s, '1', L);
    //printf("s=%d c=%d\n", strlen(s), strlen(c));
    foo();
    //printf("s=%d c=%d\n", strlen(s), strlen(c));
    //s[1024*100-1]=0;

    gettimeofday(&st,NULL);
    for( i = 0 ; i < 1000; i++ ) foo();
    gettimeofday(&et,NULL);

    printf("%f\n", time_diff(et,st));
    return 0;
}

這是Python之一:

import time

s = '1'*102400
def foo():
    c = s + s
    #assert( len(c) == 204800 )

st = time.time()
for x in xrange(1000):
    foo()
et = time.time()

print (et-st)

我得到了什么:

root@xkqeacwf:~/lab/wfaster# python cp100k.py 
0.027932882309
root@xkqeacwf:~/lab/wfaster# gcc cp100k.c
root@xkqeacwf:~/lab/wfaster# ./a.out 
0.061820

那有意義嗎? 或者我只是犯了任何愚蠢的錯誤?

累積評論(主要來自我)轉換成答案:

  • 如果你使用你對字符串長度的了解並使用memmove()memcpy()而不是strcpy()strcat()什么? (我注意到strcat()可以用strcpy()替換,結果沒有區別 - 檢查時間可能很有趣。)另外,你沒有包含<string.h> (或<stdio.h> )所以你錯過了<string.h>可能提供的任何優化!

Marcus:是的, memmove()strcpy()快,比Python快,但為什么呢? memmove()做一個字寬復制嗎?

  • 是; 在64位機器上,對於精確對齊的數據,它可以一次移動64位而不是一次移動8位; 一台32位的機器,一次可能是32位。 它在每次迭代(計數)中只有一個更簡單的測試,而不是( '計數或是空字節' )'這是一個空字節'。

Marcus:但是即使我使L=L-13 ,並且sizeof(s)給出L+1024-13memmove()仍然運行良好。 我的機器有一個sizeof(int)==4

  • memmove()的代碼是高度優化的匯編程序,可能是內聯的(沒有函數調用開銷,但對於100KiB的數據,函數調用開銷很小)。 好處來自更大的動作和更簡單的循環條件。

馬庫斯:那么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() vs memcpy() vs 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

有點奇怪的是,如果我放棄“沒有警告”並省略<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));

所以strcpy的libc版本優於gcc內置版。 (使用gdb很容易驗證strcpy上的斷點確實不會在strcpy()調用中斷開,如果包含頭文件的話)

在Linux(Debian 5.0.9,amd64)上,差異似乎可以忽略不計。 生成的程序集( -S標志)僅在包含的調試信息中有所不同。

我相信這樣做的原因是Python字符串不是以null結尾的。

在Python中,字符串長度與字符串一起存儲,允許它在連接字符串時跳過strcat()使用的隱式strlen()。

添加字符串連接直接在C中實現Python的事實可能就是原因。

編輯:好吧,現在我實際上看了C代碼,看到它使用靜態緩沖區,我也很神秘,因為我沒有看到Python如何避免動態分配,這應該慢得多......

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM