Подтвердить что ты не робот

C: скорость memcpy на динамически распределенных массивах

Мне нужна помощь в выполнении следующего кода. Он выполняет memcpy на двух динамически распределенных массивах произвольного размера:

int main()
{
  double *a, *b;
  unsigned n = 10000000, i;
  a = malloc(n*sizeof(double));
  b = malloc(n*sizeof(double));
  for(i=0; i<n; i++) {
    a[i] = 1.0;
    /* b[i] = 0.0; */
  }

  tic();
  bzero(b, n*sizeof(double));
  toc("bzero1");

  tic();
  bzero(b, n*sizeof(double));
  toc("bzero2");

  tic();
  memcpy(b, a, n*sizeof(double));
  toc("memcpy");
}

tic/toc измеряет время выполнения.

На моем компьютере он занимает 0.035s для memcpy (Linux, gcc версии 4.4.6). Если я сейчас раскомментирую строку, которая инициализирует целевой массив b, код будет в три раза быстрее (!) - 0.011 с.

Я наблюдал подобное поведение при использовании цикла вместо memcpy. Обычно меня это не волнует, так как достаточно "инициализировать" память перед ее использованием. Однако теперь мне нужно выполнить простую копию памяти и сделать это как можно быстрее. Для инициализации данных требуется запись, например. 0 в память, что не является необходимым и требует времени. И я хотел бы выполнить копию памяти со всей доступной пропускной способностью памяти.

Есть ли решение этой проблемы? Или это связано с тем, как Linux обрабатывает динамическую память (какое-то ленивое распределение страниц?) И не может быть сработано? Как это делается на других системах?

Изменить: Те же результаты получены с помощью gcc 4.6. Я использовал -O3 для компиляции.

Edit: Спасибо всем за ваши комментарии. Я понимаю, что отображение памяти требует времени. Наверное, мне просто трудно принять, что это занимает так много времени, намного дольше, чем фактический доступ к памяти. Код был изменен, чтобы включить контрольную отметку инициализации массива b, используя два последующих вызова bzero. На экране теперь отображаются

bzero1 0.273981
bzero2 0.056803
memcpy 0.117934

Очевидно, что первый вызов bzero much больше, чем просто нулевые потоки в памяти - это сопоставление памяти и обнуление памяти. Второй вызов bzero, с другой стороны, занимает половину времени, необходимого для выполнения memcpy, что точно так же, как ожидалось, - записывать только время против времени чтения и записи. Я понимаю, что накладные расходы второго вызова bzero должны быть там из-за соображений безопасности ОС. А как насчет остальных? Могу ли я каким-то образом уменьшить его? использовать большие страницы памяти? Различные настройки ядра?

Я должен упомянуть, что я запускаю это на свистке Ubuntu.

4b9b3361

Ответ 1

Первый bzero работает дольше из-за (1) ленивого распределения страниц и (2) ленивой нулевой инициализации страницы ядром. Хотя вторая причина неизбежна из-за соображений безопасности, ленивое распределение страниц может быть оптимизировано с помощью больших ( "огромных" ) страниц.

Существует как минимум два способа использования огромных страниц в Linux. Жесткий путь hugetlbfs. Простой способ Прозрачные огромные страницы.

Найдите khugepaged в списке процессов в вашей системе. Если такой процесс существует, поддерживаются прозрачные огромные страницы, вы можете использовать их в своем приложении, если вы измените malloc на это:

posix_memalign((void **)&b, 2*1024*1024, n*sizeof(double));
madvise((void *)b, n*sizeof(double), MADV_HUGEPAGE);

Ответ 2

Это, вероятно, ленивое распределение страниц, Linux только сопоставляет страницы при первом доступе. IIRC каждая страница в новом блоке в Linux - это копирование пустой записи, а ваши распределения достаточно велики, чтобы требовать новые блоки.

Если вы хотите обойти это, вы можете написать один байт или слово с интервалом 4k. Это может привести к тому, что виртуальные адреса будут отображаться в ОЗУ немного быстрее, чем писать всю страницу.

Я бы не ожидал (наиболее эффективное обходное решение, чтобы заставить ленивое сопоставление памяти происходить) плюс (копия), чтобы быть значительно быстрее, чем просто (копировать) без инициализации b. Поэтому, если нет конкретной причины, по которой вы заботитесь о производительности только копии, а не всей операции, то она довольно бесполезна. "Платите сейчас или платите позже", Linux платит позже, и вы только измеряете время позже.

Ответ 3

Конечно, если вы сравниваете скорость инициализации и копирования со скоростью только копирования, тогда инициализация должна быть включена в раздел времени. Мне кажется, вы должны сравнить это:

// Version 1
for(i=0; i<n; i++)
    a[i] = 1.0;

tic();

memcpy(b, a, n*sizeof(double));

toc();

Для этого:

// Version 2
for(i=0; i<n; i++)
    a[i] = 1.0;

tic();

for(i=0; i<n; i++)
    b[i] = 0.0;
memcpy(b, a, n*sizeof(double));

toc();

Я ожидаю, что это приведет к резкому снижению скорости 3х.

РЕДАКТИРОВАТЬ: Как предложил Стив Джессоп, вы также можете протестировать третью стратегию, касающуюся только одной записи на странице:

// Version 3
for(i=0; i<n; i++)
    a[i] = 1.0;

tic();

for(i=0; i<n; i+=DOUBLES_PER_PAGE)
    b[i] = 0.0;
memcpy(b, a, n*sizeof(double));

toc();