Intereting Posts
Неожиданный вывод основной функции путем повторного вызова Возrotation к жизни после нарушения сегментации Пропустить 2d-массив для работы в C? Имитация доступа к файловой системе в чем разница между ссылками и загрузкой на языке c Почему компилятор бросает это предупреждение: «Отсутствует инициализатор»? Не инициализирована ли структура? Valgrind сообщает об ошибке памяти при попытке освободить структуру malloc’ed статическое ключевое слово внутри массива скобки Есть ли что-то для замены функций ? Удаляет ли этот код расширение файла? C Условные соглашения 32 бит к NASM с поплавком (разность movups / movupd) Как много способов объявить String в C? (нужна помощь по модификаторам и масштабам) Каков наилучший способ вернуть ошибку из функции, когда я уже возвращаю значение? snprintf: простой способ заставить. как radix? Как проверить, начинается ли строка с определенной строки в C?

Memcpy принимает то же время, что и memset

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

Более конкретно, я запускаю более 1 ГБ массивов a и b (выделено calloc ) 100 раз со следующими операциями.

 operation time(s) ----------------------------- memset(a,0xff,LEN) 3.7 memcpy(a,b,LEN) 3.9 a[j] += b[j] 9.4 memcpy(a,b,LEN) 3.8 

Обратите внимание, что memcpy только немного медленнее, чем memset . Операции a[j] += b[j] (где j идет через [0,LEN) ) должны занимать три раза дольше, чем memcpy поскольку он работает в три раза больше данных. Однако это всего лишь около 2,5 медленнее, чем memset .

Затем я инициализировал b до нуля с memset(b,0,LEN) и снова проверил:

 operation time(s) ----------------------------- memcpy(a,b,LEN) 8.2 a[j] += b[j] 11.5 

Теперь мы видим, что memcpy примерно в два раза медленнее, чем memset а a[j] += b[j] примерно в три раза медленнее, чем memset как я ожидаю.

По крайней мере, я ожидал, что до memset(b,0,LEN) memcpy будет медленнее, поскольку ленивое выделение (первое касание) на первой из 100 итераций.

Почему я получаю только время, которое я ожидаю после memset(b,0,LEN) ?

test.c

 #include  #include  #include  void tests(char *a, char *b, const int LEN){ clock_t time0, time1; time0 = clock(); for (int i = 0; i < 100; i++) memset(a,0xff,LEN); time1 = clock(); printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC); time0 = clock(); for (int i = 0; i < 100; i++) memcpy(a,b,LEN); time1 = clock(); printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC); time0 = clock(); for (int i = 0; i < 100; i++) for(int j=0; j<LEN; j++) a[j] += b[j]; time1 = clock(); printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC); time0 = clock(); for (int i = 0; i < 100; i++) memcpy(a,b,LEN); time1 = clock(); printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC); memset(b,0,LEN); time0 = clock(); for (int i = 0; i < 100; i++) memcpy(a,b,LEN); time1 = clock(); printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC); time0 = clock(); for (int i = 0; i < 100; i++) for(int j=0; j<LEN; j++) a[j] += b[j]; time1 = clock(); printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC); } 

main.c

 #include  int tests(char *a, char *b, const int LEN); int main(void) { const int LEN = 1 << 30; // 1GB char *a = (char*)calloc(LEN,1); char *b = (char*)calloc(LEN,1); tests(a, b, LEN); } 

Скомпилируйте с помощью (gcc 6.2) gcc -O3 test.c main.c Clang 3.8 дает практически тот же результат.

Система тестирования: i7-6700HQ@2.60GHz (Skylake), 32 ГБ DDR4, Ubuntu 16.10. В моей системе Haswell полосы пропускания имеют смысл перед memset(b,0,LEN) т.е. я вижу только проблему в своей системе Skylake.

Я впервые обнаружил эту проблему из операций a[j] += b[k] в этом ответе, которая переоценивала пропускную способность.


Я придумал более простой тест

 #include  #include  #include  void __attribute__ ((noinline)) foo(char *a, char *b, const int LEN) { for (int i = 0; i < 100; i++) for(int j=0; j<LEN; j++) a[j] += b[j]; } void tests(char *a, char *b, const int LEN) { foo(a, b, LEN); memset(b,0,LEN); foo(a, b, LEN); } 

Эти выходы.

 9.472976 12.728426 

Однако, если я делаю memset(b,1,LEN) в main после calloc (см. Ниже), то он выводит

 12.5 12.5 

Это заставляет меня думать, что это проблема выделения ОС, а не проблема компилятора.

 #include  int tests(char *a, char *b, const int LEN); int main(void) { const int LEN = 1 << 30; // 1GB char *a = (char*)calloc(LEN,1); char *b = (char*)calloc(LEN,1); //GCC optimizes memset(b,0,LEN) away after calloc but Clang does not. memset(b,1,LEN); tests(a, b, LEN); } 

Дело в том, что malloc и calloc на большинстве платформ не выделяют память; они выделяют адресное пространство .

malloc т. д. работают:

  • если запрос может быть выполнен фрилистом, вырезать кусок из него
    • в случае calloc : выдается эквивалент memset(ptr, 0, size)
  • если нет: попросите ОС расширить адресное пространство.

Для систем с пейджингом спроса ( COW ) (здесь может помочь MMU), второй вариант сводится к:

  • создать достаточное количество записей в таблице таблицы для запроса и заполнить их ссылкой (COW) на /dev/zero
  • добавьте эти PTE в адресное пространство процесса

Это не будет содержать физической памяти, за исключением только таблиц страниц.

  • Как только новая память будет указана для чтения , чтение будет получено из /dev/zero . Устройство /dev/zero – это очень специальное устройство, которое в этом случае отображается на каждую страницу новой памяти.
  • но, если новая страница написана, логика COW запускается (через ошибку страницы):
    • выделяется физическая память
    • страница / dev / zero скопирована на новую страницу
    • новая страница отделена от материнской страницы
    • и вызывающий процесс может, наконец, сделать обновление, которое запустило все это

Ваш массив b вероятно, не был записан после mmap -ing (огромные запросы на распределение с malloc / calloc обычно преобразуются в mmap ). И весь массив был смоделирован до единственной «нулевой страницы только для чтения» (часть механизма COW ). Чтение нhive с одной страницы происходит быстрее, чем чтение со многих страниц, поскольку одна страница будет храниться в кеше и в TLB. Это объясняет, почему тест до memset (0) был быстрее:

Эти выходы. 9.472976 12.728426

Однако, если я делаю memset(b,1,LEN) в main после calloc (см. Ниже), то он выводит: 12.5 12.5

И еще о gcc’s malloc + memset / calloc + memset оптимизации в calloc (расширенный из моего комментария )

 //GCC optimizes memset(b,0,LEN) away after calloc but Clang does not. 

Эта оптимизация была предложена в https://gcc.gnu.org/bugzilla/show_bug.cgi?id=57742 (древовидная оптимизация PR57742) в 2013-06-27 гг. Марком Глизе ( https://stackoverflow.com/users/ 1918193 ?), Как планировалось для версии 4.9 / 5.0 GCC:

memset (malloc (n), 0, n) -> calloc (n, 1)

calloc иногда может быть значительно быстрее, чем malloc + bzero, потому что у него есть особые знания о том, что некоторая память уже равна нулю. Когда другие оптимизации упрощают некоторый код до malloc + memset (0), было бы неплохо заменить его calloc. К сожалению, я не думаю, что есть способ сделать подобную оптимизацию в C ++ с новым, где именно такой код легче всего выглядит (например, создание std :: vector (10000)). И там также будет сложность в том, что размер memset будет немного меньше, чем размер malloc (использование calloc все равно будет прекрасным, но становится все труднее узнать, является ли это улучшением).

Реализовано в 2014-06-24 ( https://gcc.gnu.org/bugzilla/show_bug.cgi?id=57742#c15 ) – https://gcc.gnu.org/viewcvs/gcc?view=revision&revision=211956 (также https://patchwork.ozlabs.org/patch/325357/ )

  • tree-ssa-strlen.c … (handle_builtin_malloc, handle_builtin_memset): новые функции.

Текущий код в gcc/tree-ssa-strlen.c https://github.com/gcc-mirror/gcc/blob/7a31ada4c400351a35ab65f8dc0357e7c88805d5/gcc/tree-ssa-strlen.c#L1889 – если memset(0) получает указатель от malloc или calloc , он преобразует malloc в calloc а затем memset(0) будет удален:

 /* Handle a call to memset. After a call to calloc, memset(,0,) is unnecessary. memset(malloc(n),0,n) is calloc(n,1). */ static bool handle_builtin_memset (gimple_stmt_iterator *gsi) ... if (code1 == BUILT_IN_CALLOC) /* Not touching stmt1 */ ; else if (code1 == BUILT_IN_MALLOC && operand_equal_p (gimple_call_arg (stmt1, 0), size, 0)) { gimple_stmt_iterator gsi1 = gsi_for_stmt (stmt1); update_gimple_call (&gsi1, builtin_decl_implicit (BUILT_IN_CALLOC), 2, size, build_one_cst (size_type_node)); si1->length = build_int_cst (size_type_node, 0); si1->stmt = gsi_stmt (gsi1); } 

Это обсуждалось в списке рассылки gcc-patches с 1 марта 2014 года по 15 июля 2014 года с темой « calloc = malloc + memset »

с заметным комментарием от Andi Kleen ( http://halobates.de/blog/ , https://github.com/andikleen ): https://gcc.gnu.org/ml/gcc-patches/2014-06/msg01818 .html

FWIW Я считаю, что преобразование приведет к разрыву большого количества микро-тестов.

calloc внутренне знает, что память, свежая от ОС, обнуляется. Но память еще не может быть ошибочной.

memset всегда memset ошибки в памяти.

Поэтому, если у вас есть тест, например

  buf = malloc(...) memset(buf, ...) start = get_time(); ... do something with buf end = get_time() 

Теперь время будет полностью отключено, потому что измеренное время включает ошибки страницы.

Марк ответил: « Хороший момент. Думаю, что работа над оптимизацией компилятора является частью игры для микро-тестов, и их авторы будут разочарованы, если компилятор не будет регулярно ее испортить новыми и развлекательными способами 😉 », и Анди спросил: « Я бы предпочел не делать этого. Я не уверен, что у него много пользы. Если вы хотите сохранить его, пожалуйста, убедитесь, что есть простой способ отключить его ».

Марк показывает, как отключить эту оптимизацию: https://gcc.gnu.org/ml/gcc-patches/2014-06/msg01834.html

Любой из этих флагов работает:

  • -fdisable-tree-strlen
  • -fno-builtin-malloc
  • -fno-builtin-memset (при условии, что вы явно написали «memset» в своем коде)
  • -fno-builtin
  • -ffreestanding
  • -O1
  • -Os

В коде вы можете скрыть, что указатель, переданный в memset является возвращаемым malloc , сохраняя его в переменной volatile или любой другой трюк, чтобы скрыть от компилятора, который мы делаем memset(malloc(n),0,n) .