Скопирует ли strncpy / memcpy / memmove данные побайтно или иным способом? - PullRequest
0 голосов
/ 22 января 2019

Как мы знаем, в многобайтовом словесном компьютере, таком как x86 / x86_64, более эффективно копировать / перемещать большую часть слова памяти за словом (4 или 8 байт на шаг), чем делать это байт byte.

Мне интересно, каким образом strncpy / memcpy / memmove будет что-то делать, и как они справляются с выравниванием слов памяти.

char buf_A[8], buf_B[8];

// I often want to code as this
*(double*)buf_A = *(double*)buf_B;

//in stead of this
strcpy(buf_A, buf_B);
// but it worsen the readability of my codes.

Ответы [ 6 ]

0 голосов
/ 22 января 2019

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

К моему удивлению, самый быстрый метод не тот, который мы ожидали теоретически.

Я попробовал какой-то код, как показано ниже.

#include <cstring>
#include <iostream>
#include <string>
#include <chrono>

using std::string;
using std::chrono::system_clock;

inline void mycopy( double* a, double* b, size_t s ) {
   while ( s > 0 ) {
      *a++ = *b++;
      --s;
   }
};

// to make sure that every bits have been changed
bool assertAllTrue( unsigned char* a, size_t s ) {
   unsigned char v = 0xFF;
   while ( s > 0 ) {
      v &= *a++;
      --s;
   }
   return v == 0xFF;
};

int main( int argc, char** argv ) {
   alignas( 16 ) char bufA[512], bufB[512];
   memset( bufB, 0xFF, 512 );  // to prevent strncpy from stoping prematurely
   system_clock::time_point startT;

   memset( bufA, 0, sizeof( bufA ) );
   startT = system_clock::now();
   for ( int i = 0; i < 1024 * 1024; ++i )
      strncpy( bufA, bufB, sizeof( bufA ) );
   std::cout << "strncpy:" << ( system_clock::now() - startT ).count()
             << ", AllTrue:" << std::boolalpha
             << assertAllTrue( ( unsigned char* )bufA, sizeof( bufA ) )
             << std::endl;

   memset( bufA, 0, sizeof( bufA ) );
   startT = system_clock::now();
   for ( int i = 0; i < 1024 * 1024; ++i )
      memcpy( bufA, bufB, sizeof( bufA ) );
   std::cout << "memcpy:" << ( system_clock::now() - startT ).count()
             << ", AllTrue:" << std::boolalpha
             << assertAllTrue( ( unsigned char* )bufA, sizeof( bufA ) )
             << std::endl;

   memset( bufA, 0, sizeof( bufA ) );
   startT = system_clock::now();
   for ( int i = 0; i < 1024 * 1024; ++i )
      memmove( bufA, bufB, sizeof( bufA ) );
   std::cout << "memmove:" << ( system_clock::now() - startT ).count()
             << ", AllTrue:" << std::boolalpha
             << assertAllTrue( ( unsigned char* )bufA, sizeof( bufA ) )
             << std::endl;

   memset( bufA, 0, sizeof( bufA ) );
   startT = system_clock::now();
   for ( int i = 0; i < 1024 * 1024; ++i )
      mycopy( ( double* )bufA, ( double* )bufB, sizeof( bufA ) / sizeof( double ) );
   std::cout << "mycopy:" << ( system_clock::now() - startT ).count()
             << ", AllTrue:" << std::boolalpha
             << assertAllTrue( ( unsigned char* )bufA, sizeof( bufA ) )
             << std::endl;

   return EXIT_SUCCESS;
}

Результат (один из многих похожих результатов):

strncpy: 52840919, AllTrue: true

memcpy: 57630499, AllTrue: true

memmove: 57536472, AllTrue: true

mycopy: 57577863, AllTrue: true

Похоже:

  1. memcpy, memmove и мой собственный метод имеют схожий результат;
  2. Что делает strncpy с магией, так что он лучший даже быстрее чем memcpy?

Это смешно?

0 голосов
/ 22 января 2019

Это зависит от того, какой компилятор вы используете, и библиотеки времени выполнения C, которую вы используете. В большинстве случаев функции string.h, такие как memcmp, memcpy, strcpu, memset и т. Д., Реализованы с использованием сборки оптимизированным для ЦП способом.

Вы можете найти в GNU libc реализации этих функций для архитектуры AMD64 . Как вы можете видеть, он может использовать инструкции SSE или AVX для копирования 128 и 512 бит на итерацию. Microsoft также связывает исходный код своего CRT вместе с Visual Studio (в основном те же подходы, поддерживаются циклы MMX, SSE, AVX).

Также компилятор использует специальную оптимизацию для таких функций, GCC вызывает их builtins Другие компиляторы называют их внутренними. То есть Компилятор может выбрать - вызвать библиотечную функцию или сгенерировать специфичный для CPU код сборки, оптимальный для текущего контекста. Например, когда N аргумент memcpy является постоянным, то есть memcpy(dst, src, 128) компилятор может генерировать встроенный код сборки (что-то вроде mov 16,rcx cls rep stosq), а когда это переменная, например memcpy(dst,src,bytes) - компилятор может вставить вызов функции библиотеки (что-то вроде call _memcpy)

0 голосов
/ 22 января 2019

В этом случае вы можете использовать memcpy, поскольку это эквивалент *(double*)buf_A = *(double*)buf_B; без неопределенного поведения.

Вам не следует беспокоиться о вызове memcpy, поскольку по умолчанию компилятор предполагает, чтоВызов memcpy имеет значение, определенное в библиотеке c.Таким образом, в зависимости от типа аргумента и / или знания размера копии во время компиляции, компилятор может решить не вызывать функцию библиотеки c и встроить более адаптированную стратегию копирования памяти.В gcc вы можете отключить это поведение с помощью опции компилятора -fno-builtin: demo .

Требуется замена вызова memcpy компилятором, поскольку memcpy будет проверять размер и выравнивание указателей.использовать наиболее эффективную стратегию копирования памяти (может начинаться копирование как маленьких блоков, так и символов за символом, в очень большие блоки, например, с помощью инструкции AVX512).Эти проверки и все, что нужно для вызова memcpy.

Также Если вы ищете эффективность, вы должны быть обеспокоены выравниванием памяти.Поэтому вы можете объявить выравнивание вашего буфера:

alignas(8) char buf_A[8];
0 голосов
/ 22 января 2019

В общем, вам не нужно слишком много думать о том, как memcpy или другие подобные функции реализованы. Вы должны предполагать, что они эффективны, если только ваше профилирование не докажет, что вы неправы.

На практике это действительно хорошо оптимизировано. Смотрите, например, следующий тестовый код:

#include <cstring>

void test(char (&a)[8], char (&b)[8])
{
    std::memcpy(&a,&b,sizeof a);
}

Компиляция с помощью g ++ 7.3.0 с помощью команды g++ test.cpp -O3 -S -masm=intel мы можем увидеть следующий код сборки:

test(char (&) [8], char (&) [8]):

    mov     rax, QWORD PTR [rsi]
    mov     QWORD PTR [rdi], rax
    ret

Как видите, копия не только встроена, но и свернута в одно 8-байтовое чтение и запись.

0 голосов
/ 22 января 2019

Скопирует ли strcpy / strncpy данные побайтно или иным способом?

Стандарты C ++ и C не определяют, как именно реализованы strcpy / strncpy. Они только описывают поведение.

Существует несколько реализаций стандартной библиотеки, и каждая выбирает, как реализовать свои функции. Возможно реализовать оба из них, используя memcpy. Стандарты также точно не описывают реализацию memcpy, и существование нескольких реализаций также применимо к ней.

memcpy может быть реализован с использованием полного копирования слов. Короткий псевдокод того, как memcpy может быть реализован:

if len >= 2 * word size
    copy bytes until destination pointer is aligned to word boundary
    if len >= page size
        copy entire pages using virtual address manipulation
    copy entire words
 copy the trailing bytes that are not aligned to word boundary

Чтобы узнать, как конкретная реализация стандартной библиотеки реализует strcpy / strncpy / memcpy, вы можете прочитать исходный код стандартной библиотеки - если у вас есть к нему доступ.

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

0 голосов
/ 22 января 2019

С cpp-ссылка :

Копирует количество байтов из объекта, на который указывает src, в объект, на который указывает dest. Оба объекта интерпретируются как массивы без знака.

ПРИМЕЧАНИЯ

std :: memcpy предназначен для самой быстрой библиотечной процедуры для копирования из памяти в память. Обычно он более эффективен, чем std :: strcpy, который должен сканировать данные, которые он копирует, или std :: memmove, который должен принимать меры предосторожности для обработки перекрывающихся входных данных.

Несколько компиляторов C ++ преобразуют подходящие циклы копирования памяти в вызовы std :: memcpy.

Если строгое псевдонимы запрещают проверять ту же память, что и значения двух разных типов, для преобразования значений можно использовать std :: memcpy.

Так что это должен быть самый быстрый способ копирования данных. Имейте в виду, однако, что есть несколько случаев, когда поведение не определено:

Если объекты перекрываются, поведение не определено.

Если dest или src - нулевой указатель, поведение не определено, даже если count равен нулю.

Если объекты потенциально перекрываются или не могут быть TriviallyCopyable, поведение memcpy не указано и может быть неопределенным.

...