Производительность memcpy() — Ubuntu x86_64

Я наблюдаю какое-то странное поведение, которое не могу объяснить. Ниже приведены подробности: -

#include <sched.h>
#include <sys/resource.h>
#include <time.h>
#include <iostream>

void memcpy_test() {
    int size = 32*4;
    char* src = new char[size];
    char* dest = new char[size];
    general_utility::ProcessTimer tmr;
    unsigned int num_cpy = 1024*1024*16; 
    struct timespec start_time__, end_time__;
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start_time__);
    for(unsigned int i=0; i < num_cpy; ++i) {
        __builtin_memcpy(dest, src, size);
    }
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start_time__);
    std::cout << "time = " << (double)(end_time__.tv_nsec - start_time__.tv_nsec)/num_cpy << std::endl;
    delete [] src;
    delete [] dest;
}

Когда я указываю -march=native в опциях компилятора, сгенерированный бинарник работает в 2,7 раза медленнее. Это почему ? Во всяком случае, я ожидаю, что -march=native создаст оптимизированный код. Существуют ли другие функции, которые могли бы показать этот тип поведения?

РЕДАКТИРОВАТЬ 1: Еще один интересный момент заключается в том, что если размер> 32 * 4, то нет разницы между временем выполнения генерируемых таким образом двоичных файлов.

РЕДАКТИРОВАТЬ 2: Ниже приведен подробный анализ производительности (__builtin_memcpy()): –

size = 32 * 4, без -march=native - 7,5 нс, с -march=native - 19,3

size = 32 * 8, без -march=native - 26,3 нс, с -march=native - 26,5

РЕДАКТИРОВАТЬ 3:

Это наблюдение не изменится, даже если я выделю int64_t/int32_t.

РЕДАКТИРОВАТЬ 4:

size = 8192, без -march=native ~ 2750 нс, с -march=native ~ 2750 (Ранее была ошибка в сообщении этого числа, ошибочно записывалось как 26,5, теперь правильно )

Я запускал их много раз, и цифры одинаковы для каждого запуска.


person Faraz    schedule 23.07.2011    source источник
comment
и каковы результаты, если размер (действительно) большой? возможно, он оптимизирует его, но для больших блоков.   -  person Karoly Horvath    schedule 23.07.2011
comment
Кроме того, что вы измеряете с помощью простого memcpy?   -  person Karoly Horvath    schedule 23.07.2011
comment
А что такое действительно большой размер?   -  person Faraz    schedule 23.07.2011
comment
@Faraz, вы используете массивы char. AFAICT выровнены по байтам. __builtin_memcpy() может быть оптимизирован для более высоких границ выравнивания под -march=native на вашей платформе. Можете ли вы попробовать с массивами int или long?   -  person Frédéric Hamidi    schedule 23.07.2011
comment
Я не знаю... просто попробуйте найти размер, при котором скомпилированный код -march=native превосходит обычный.   -  person Karoly Horvath    schedule 23.07.2011
comment
@Frédéric: я проголосовал за это, но теперь я понял, что это не будет правдой, поскольку распределитель памяти выровняет это до 4 или 8 байтов.   -  person Karoly Horvath    schedule 23.07.2011
comment
@yi_H, какой распределитель памяти? ::operator new[]? Зачем это делать? :)   -  person Frédéric Hamidi    schedule 23.07.2011
comment
нет никакой разницы, когда я выделяю int64_t.   -  person Faraz    schedule 23.07.2011
comment
@Faraz, ты имеешь в виду no difference with or without -march=native (проблема решена) или no difference compared to the behavior with char (возврат к исходной точке)?   -  person Frédéric Hamidi    schedule 23.07.2011
comment
@Frédéric: для простоты, по соображениям производительности (вы должны получить доступ к этой выделенной памяти позже), и выделение ее в произвольной позиции, вероятно, также плохо для фрагментации. на самом деле он, вероятно, добавит байты заполнения после него, чтобы он заканчивался на большей границе. (например: у него есть отдельные страницы для фрагментов длиной 32, 64, 128, 256 байт, и он вернет свободный фрагмент из того, который может содержать как запрошенные данные, так и дополнительную информацию об обслуживании)   -  person Karoly Horvath    schedule 23.07.2011
comment
@yi_H, для больших блоков я не получаю последовательных результатов, в основном результаты различаются для разных прогонов, поэтому мне кажется, что для больших блоков есть некоторый сбой измерения. Любые идеи ?   -  person Faraz    schedule 23.07.2011
comment
О, понял, поправлю. Не учитывать секунды для программ, которые будут работать более 1 секунды   -  person Faraz    schedule 23.07.2011


Ответы (2)


Я повторил ваши выводы с: g++ (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2, Linux 2.6.38-10-generic #46-Ubuntu x86_64 на моем Core 2 Duo. Результаты, вероятно, будут различаться в зависимости от версии вашего компилятора и процессора. Я получаю ~ 26 и ~ 9.

Когда я указываю -march=native в опциях компилятора, сгенерированный бинарник работает в 2,7 раза медленнее. Это почему ?

Поскольку версия -march=native компилируется в (найденная с помощью objdump -D, вы также можете использовать gcc -S -fverbose-asm):

    rep movsq %ds:(%rsi),%es:(%rdi) ; where rcx = 128 / 8

И версия без компилируется в 16 пар загрузки/сохранения, например:

    mov    0x20(%rbp),%rdx
    mov    %rdx,0x20(%rbx)

Что, по-видимому, быстрее на наших компьютерах.

Во всяком случае, я ожидаю, что -march=native создаст оптимизированный код.

В данном случае оказалось пессимистичным отдавать предпочтение rep movsq серии ходов, но это не всегда так. Первая версия короче, что может быть лучше в некоторых (большинстве?) случаях. Или это может быть ошибка в оптимизаторе.

Существуют ли другие функции, которые могли бы показать этот тип поведения?

Любая функция, для которой сгенерированный код отличается при указании -march=native, к подозреваемым относятся функции, реализованные в виде макросов или статики в заголовках, имеет имя, начинающееся с __builtin. Возможно также (с плавающей запятой) математические функции.

Еще один интересный момент заключается в том, что если размер > 32 * 4, то нет никакой разницы между временем выполнения сгенерированных таким образом двоичных файлов.

Это потому, что тогда они оба компилируются в rep movsq, 128, вероятно, является самым большим размером, для которого GCC будет генерировать серию загрузок/сохранений (было бы интересно посмотреть, будет ли это также для других платформ). Кстати, когда компилятор не знает размер во время компиляции (например, int size=atoi(argv[1]);), он просто превращается в вызов memcpy с переключателем или без него.

person user786653    schedule 23.07.2011

Это довольно известная проблема (и очень старая).

http://gcc.gnu.org/bugzilla/show_bug.cgi?id=43052

посмотрите на какой-нибудь нижний комментарий в отчете об ошибке:

«Просто к вашему сведению: теперь mesa по умолчанию использует -fno-builtin-memcmp для обхода этой проблемы»

Похоже, memcpy в glibc намного лучше, чем встроенный...

person Piotr Niemcunowicz    schedule 20.02.2013