zed.0xff.me

benchmark: Ruby's rb_str_cat() vs C's strcat()

В связи с началом написания расширений для ruby решил провести бенчмарк рубевской функции rb_str_cat() VS сишной родной strcat()
Результат меня немало удивил:

1
2
strcat     3.360s
rb_str_cat 0.002s

Тесты проводились в тяжелых полевых условиях – испытуемым надо было 100000 (сто тысяч) раз приклеить к строке(изначально пустой) один заранее заданный символ.
И ruby сделал это, грубо говоря, в 1500 раз быстрее!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define N 100000

VALUE test_concat_rb_str_cat(VALUE self){
    int i;
    VALUE s = rb_str_new2("");

    for(i=0;i<N;i++) rb_str_cat(s,"t",1);
    return s;
}

VALUE test_concat_strcat(VALUE self){
    int i;
    char buf[N+2];

    *buf=0;
    for(i=0;i<N;i++) strcat(buf,"t");
    return rb_str_new2(buf);
}

На самом деле никакого подвоха тут нет – просто в руби объект “строка” содержит в себе не только указатель на байты содержимого, но и длину строки тоже, а сишный вариант при этом каждый раз считает длину строки. Т.е. ему, бедному, пришлось перемолотить при этом суммарно почти 5 гигабайт, каждый раз считая длину строки начиная с самого первого символа, и до 100-тысячного в конце.

Прелесть руби здесь в том что не надо думать о памяти – он сам выделит сколько надо и освободит когда будет можно. В сях это слегка сложнее, но можно доработать сишный код так, чтобы он и память выделял динамически, и длину строки стопицот раз не пересчитывал. Например, так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
VALUE test_concat_char(VALUE self){
    int i;
    int bufsize = 0x1000;
    char *buf = malloc(bufsize);
    char *p=buf;
    VALUE s;

    for(i=0;i<N;i++){
        *p++ = 't';
        if( p-buf >= bufsize ){
            char *oldbuf = buf;
            bufsize += 0x1000;
            buf = realloc(buf, bufsize);
            p = buf + (p-oldbuf);
        }
    }
    s=rb_str_new(buf,p-buf);
    free(buf);
    return s;
}

Скорость обработки получается уже совсем другая:

1
2
3
strcat     3.36732s
rb_str_cat 0.00215s
char       0.00013s

Вот теперь сишный код в 16 раз быстрее чем руби, и в 24000 с лишним раз быстрее, чем в начале.

PS: это всё на Linux 2.6.31-gentoo-r6-zz x86_64 Intel(R) Core(TM)2 Duo CPU E8400 @ 3.00GHz