2
votes

Est-ce que strncpy / memcpy / memmove copie les données octet par octet ou d'une autre manière efficace?

Comme nous le savons, dans un ordinateur de mots multi-octets tel que x86 / x86_64, il est plus efficace de copier / déplacer une grande partie de la mémoire mot par mot (4 ou 8 octets par pas), que de le faire octet par octet.

Je suis curieux de savoir de quelle manière strncpy / memcpy / memmove ferait les choses, et comment gèrent-ils l'alignement des mots de mémoire.

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.


4 commentaires

Le fonctionnement de strcpy et de ses amis dépend totalement de l'implémentation. Habituellement, ils font simplement le travail de manière efficace. Oh oui et strcpy ne peuvent être utilisés que pour copier des chaînes terminées par NUL (lisez le chapitre traitant des chaînes dans votre manuel C). Dans votre cas, vous devez utiliser memcpy .


Sur la plupart des plates-formes, le code source de memcpy etc. est inclus, regardez-les, mais le code peut être trop surprenant. Ils sont également souvent écrits à la main en langage d'assemblage.


Merci, Jabberwocky! J'utilise gcc 8.2.0 dans un fichier x86_64. Pourriez-vous s'il vous plaît me dire où puis-je trouver la source du memcpy / strcpy? J'ai toute la source de gcc, mais je ne sais pas y nager. C'est vraiment un océan.


Sauf si vous avez une bonne raison de penser le contraire, vous devez supposer que l'implémenteur de bibliothèque standard connaît mieux que vous le matériel cible et les exigences de la bibliothèque. Ce n'est pas une réprobation, juste un commentaire sur l'expertise.


6 Réponses :


1
votes

De cpp-reference :

Copie count octets de l'objet pointé par src vers l'objet pointé par dest. Les deux objets sont réinterprétés comme des tableaux de caractères non signés.

std :: memcpy est censé être la routine de bibliothèque la plus rapide pour la copie de mémoire à mémoire. Il est généralement plus efficace que std :: strcpy, qui doit analyser les données qu'il copie ou std :: memmove, qui doit prendre des précautions pour gérer les entrées qui se chevauchent.

Plusieurs compilateurs C ++ transforment les boucles de copie mémoire appropriées en appels std :: memcpy.

Là où l'aliasing strict interdit d'examiner la même mémoire que des valeurs de deux types différents, std :: memcpy peut être utilisé pour convertir les valeurs.

Cela devrait donc être le moyen le plus rapide de copier des données. Sachez cependant qu'il existe plusieurs cas où le comportement n'est pas défini:

Si les objets se chevauchent, le comportement n'est pas défini.

Si dest ou src est un pointeur nul, le comportement n'est pas défini, même si count vaut zéro.

Si les objets se chevauchent potentiellement ou ne sont pas TriviallyCopyable, le comportement de memcpy n'est pas spécifié et peut être indéfini.


2 commentaires

Je vous remercie! Mais je veux toujours savoir de quelle manière std :: memcpy fait les choses. Octet par octet, ou mot par mot?


Vous copiez un certain nombre d'octets. Il n'y a aucune garantie de mise en œuvre.



1
votes

Est-ce que strcpy / strncpy copie les données octet par octet ou d'une autre manière efficace?

Le standard C ++ ni C ne spécifie pas comment strcpy / strncpy est implémenté exactement. Ils ne décrivent que le comportement.

Il existe plusieurs implémentations de bibliothèques standard et chacune choisit comment implémenter ses fonctions. Il est possible d'implémenter les deux en utilisant memcpy. Les standards ne décrivent pas exactement l'implémentation de memcpy non plus, et l'existence de plusieurs implémentations s'y applique tout aussi bien.

memcpy peut être implémentée en tirant parti de la copie complète des mots. Un court pseudocode expliquant comment memcpy pourrait être implémenté:

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

Pour découvrir comment une implémentation de bibliothèque standard particulière implémente strcpy / strncpy / memcpy, vous pouvez lisez le code source de la bibliothèque standard - si vous y avez accès.

Encore plus, lorsque la longueur est connue au moment de la compilation, le compilateur peut même choisir de ne pas utiliser la bibliothèque memcpy, mais à la place la copie en ligne. Si votre compilateur a intégré des définitions pour les fonctions de bibliothèque standard, vous pouvez le découvrir dans la documentation du compilateur respectif.


0 commentaires

4
votes

En général, vous n'avez pas à trop réfléchir à la manière dont memcpy ou d'autres fonctions similaires sont implémentées. Vous devez supposer qu'ils sont efficaces à moins que votre profilage ne prouve que vous avez tort.

En pratique, il est en effet bien optimisé. Voir par exemple le code de test suivant:

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

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

En le compilant avec g ++ 7.3.0 avec la commande g ++ test.cpp -O3 -S -masm = intel nous peut voir le code d'assemblage suivant:

#include <cstring>

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

Comme vous pouvez le voir, la copie est non seulement incorporée, mais également réduite en une seule lecture et écriture de 8 octets.


0 commentaires

2
votes

Dans ce cas, vous pouvez préférer utiliser memcpy car c'est l'équivalent de * (double *) buf_A = * (double *) buf_B; sans comportement indéfini.

Vous ne devez pas vous soucier d'appeler memcpy car par défaut le compilateur suppose qu'un appel à memcpy a la signification définie dans la bibliothèque c. Ainsi, selon le type de l'argument et / ou la connaissance de la taille de la copie au moment de la compilation, le compilateur peut choisir de ne pas appeler la fonction de bibliothèque c et d'intégrer une stratégie de copie mémoire plus adaptée. Sur gcc, vous pouvez désactiver ce comportement avec l'option du compilateur -fno-builtin : demo a>.

Le remplacement de l'appel memcpy par le compilateur est souhaité car memcpy vérifiera la taille et l'alignement des pointeurs pour utiliser la stratégie de copie mémoire la plus efficace (il peut commencer à copier aussi petits blocs que char par char en très gros blocs en utilisant l'instruction AVX512 par exemple). Ces vérifications et que ce soit l'appel à memcpy coûtent.

Aussi, si vous recherchez l'efficacité, vous devriez vous préoccuper de l'alignement de la mémoire. Vous voudrez peut-être déclarer l'alignement de votre tampon:

alignas(8) char buf_A[8];


0 commentaires

1
votes

Cela dépend du compilateur que vous utilisez et de la bibliothèque d'exécution C que vous utilisez. Dans la plupart des cas, les fonctions string.h telles que memcmp , memcpy , strcpu , memset , etc. implémentées en utilisant l'assemblage dans le CPU manière optimisée.

Vous pouvez trouver les implémentations GNU libc de ces fonctions pour l'AMD64 architecture . Comme vous pouvez le voir, il peut utiliser des instructions SSE ou AVX pour copier 128 et 512 bits par itération. Microsoft regroupe également le code source de leur CRT avec Visual Studio (les mêmes approches généralement, les boucles MMX, SSE, AVX sont prises en charge).

Le compilateur utilise également une optimisation spéciale pour ces fonctions, GCC les appelle builtins autre compilateur les appelle intrinsèques. C'est à dire. le compilateur peut choisir - appeler une fonction de bibliothèque, ou générer un code d'assemblage spécifique au processeur optimal pour le contexte actuel. Par exemple, lorsque l'argument N de memcpy est constant, c'est-à-dire que le compilateur memcpy (dst, src, 128) peut générer un code d'assemblage en ligne (quelque chose comme < code> mov 16, rcx cls rep stosq ), et quand il s'agit d'une variable ie memcpy (dst, src, bytes) - le compilateur peut insérer un appel à la fonction de bibliothèque (quelque chose comme appeler _memcpy )


0 commentaires

0
votes

Je pense que tous les avis et conseils sur cette page sont raisonnables, mais je décide d'essayer une petite expérience.

À ma grande surprise, la méthode la plus rapide n'est pas celle que nous attendions théoriquement.

J'ai essayé du code comme suit.

#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;
}

Le résultat (l'un des nombreux résultats similaires):

strncpy: 52840919, AllTrue: true

memcpy: 57630499, AllTrue: true

memmove: 57536472, AllTrue: true

mycopie: 57577863, AllTrue: true

Cela ressemble à:

  1. memcpy, memmove et ma propre méthode ont un résultat similaire;
  2. Qu'est-ce que strncpy fait de la magie, pour que ce soit le meilleur encore plus rapide que memcpy?

Est-ce drôle?


2 commentaires

Maintenant, je peux utiliser strncpy sans aucun souci!


memcpy, memmove et ma propre méthode ont un résultat similaire; - parce que le compilateur a remplacé votre méthode par l'appel de la bibliothèque à memcpy pendant l'optimisation. Si vous désactivez l'optimisation, vous verrez la différence.