Я збираюся йти проти загальної мудрості, яка std::copy
матиме невелику, майже непомітну втрату продуктивності. Я щойно робив тест і виявив, що це неправда: я помітив різницю у виконанні. Однак переможець був std::copy
.
Я написав реалізацію C ++ SHA-2. У своєму тесті я хешував 5 рядків, використовуючи всі чотири версії SHA-2 (224, 256, 384, 512), і я цикл 300 разів. Я вимірюю рази за допомогою Boost.timer. Цього лічильника на 300 циклів достатньо, щоб повністю стабілізувати мої результати. Я провів тест по 5 разів кожен, чергуючи memcpy
версію і std::copy
версію. Мій код використовує переваги захоплення даних на якомога більшій кількості фрагментів (багато інших реалізацій працюють з char
/ char *
, тоді як я працюю з T
/ T *
(де T
найбільший тип в реалізації користувача, який має правильну поведінку переповнення), тому швидкий доступ до пам'яті на Найбільші типи, які я можу, є основними в роботі мого алгоритму. Це мої результати:
Час (в секундах) для завершення запуску тестів SHA-2
std::copy memcpy % increase
6.11 6.29 2.86%
6.09 6.28 3.03%
6.10 6.29 3.02%
6.08 6.27 3.03%
6.08 6.27 3.03%
Загальне середнє збільшення швидкості std :: копія через memcpy: 2,99%
Мій компілятор - gcc 4.6.3 у Fedora 16 x86_64. Мої прапори з оптимізації є -Ofast -march=native -funsafe-loop-optimizations
.
Код моїх реалізацій SHA-2.
Я вирішив також запустити тест на мою реалізацію MD5. Результати були набагато менш стабільними, тому я вирішив зробити 10 пробіжок. Однак після моїх перших спроб я отримав результати, які дивовижно змінювались від одного запуску до другого, тож я здогадуюсь, що там відбувалася якась діяльність ОС. Я вирішив почати спочатку.
Ті ж настройки компілятора та прапори. Є лише одна версія MD5, і вона швидша, ніж SHA-2, тому я зробив 3000 циклів на подібному наборі з 5 тестових рядків.
Це мої останні 10 результатів:
Час (в секундах) для завершення виконання тестів MD5
std::copy memcpy % difference
5.52 5.56 +0.72%
5.56 5.55 -0.18%
5.57 5.53 -0.72%
5.57 5.52 -0.91%
5.56 5.57 +0.18%
5.56 5.57 +0.18%
5.56 5.53 -0.54%
5.53 5.57 +0.72%
5.59 5.57 -0.36%
5.57 5.56 -0.18%
Загальне середнє зниження швидкості std :: копія через memcpy: 0,11%
Код для моєї реалізації MD5
Ці результати говорять про те, що існує певна оптимізація, що std :: copy використовується в моїх тестах SHA-2, які std::copy
не вдалося використати в моїх тестах MD5. У тестах SHA-2 обидва масиви були створені в одній функції, що викликала std::copy
/ memcpy
. У моїх тестах на MD5 один із масивів передався функції як функціональний параметр.
Я зробив трохи більше тестування, щоб побачити, що я можу зробити, щоб зробити std::copy
швидше знову. Відповідь виявилася простою: увімкніть оптимізацію часу зв'язку. Це мої результати з увімкненою LTO (опція -flto в gcc):
Час (в секундах) на завершення виконання тестів MD5 за допомогою -flto
std::copy memcpy % difference
5.54 5.57 +0.54%
5.50 5.53 +0.54%
5.54 5.58 +0.72%
5.50 5.57 +1.26%
5.54 5.58 +0.72%
5.54 5.57 +0.54%
5.54 5.56 +0.36%
5.54 5.58 +0.72%
5.51 5.58 +1.25%
5.54 5.57 +0.54%
Загальне середнє збільшення швидкості std :: копіювання через memcpy: 0,72%
Підводячи підсумок, за використання не видається покарання за ефективність std::copy
. Насправді, схоже, є підвищення продуктивності.
Пояснення результатів
То чому б std::copy
це може збільшити продуктивність?
По-перше, я не очікував би, що це буде повільніше для будь-якої реалізації, доки не буде включена оптимізація вбудовування. Всі компілятори вбудовуються агресивно; це, мабуть, найважливіша оптимізація, оскільки вона дозволяє так багато інших оптимізацій. std::copy
може (і я підозрюю, що це реально реалізує у всьому світі) виявити, що аргументи тривіально копіюються і пам'ять викладається послідовно. Це означає, що в гіршому випадку, коли memcpy
це законно, std::copy
слід виконувати не гірше. Тривіальна реалізація, std::copy
яка відкладає, memcpy
повинна відповідати критеріям вашого компілятора: "завжди вказуйте це під час оптимізації для швидкості чи розміру".
Однак std::copy
також зберігає більше своєї інформації. Під час дзвінка std::copy
функція зберігає типи недоторканими. memcpy
працює на void *
, що відкидає майже всю корисну інформацію. Наприклад, якщо я передаю масив std::uint64_t
, компілятор або реалізатор бібліотеки, можливо, зможуть скористатися 64-бітним вирівнюванням std::copy
, але це може бути складніше memcpy
. Багато реалізацій алгоритмів, подібних до цієї роботи, спочатку працюють над неврівноваженою частиною на початку діапазону, потім вирівняною частиною, потім нерівномірною частиною в кінці. Якщо все гарантовано вирівняно, код стане простішим та швидшим, і передбачувач гілок у вашому процесорі стане легшим.
Передчасна оптимізація?
std::copy
знаходиться в цікавому положенні. Я очікую, що це ніколи не буде повільніше, memcpy
а іноді і швидше з будь-яким сучасним оптимізуючим компілятором. Більше того, все, що можна memcpy
, ти можеш std::copy
. memcpy
не дозволяє перекривати буфери, тоді як std::copy
підтримує перекриття в одному напрямку ( std::copy_backward
для іншого напрямку перекриття). memcpy
працює тільки на покажчики, std::copy
працює на будь-яких ітератори ( std::map
, std::vector
, std::deque
, або мій власний користувальницький тип). Іншими словами, ви повинні просто використовувати, std::copy
коли вам потрібно копіювати фрагменти даних навколо.
char
може бути підписано або без підпису, залежно від реалізації. Якщо кількість байтів може бути> = 128, тоді використовуйтеunsigned char
для своїх байтових масивів. (У(int *)
ролях було б і безпечніше(unsigned int *)
.)