web-dev-qa-db-fra.com

C: Quel est le meilleur moyen et le plus rapide de concaténer des chaînes

Je concatène actuellement des chaînes dans c en utilisant la fonction strcat() de la bibliothèque string.h.

J'y ai réfléchi et je suis arrivé à la conclusion que ce devrait être une fonction très coûteuse, car avant de commencer à concaténer, il doit itérer sur le tableau de caractères jusqu'à ce qu'il trouve le caractère '\0'.

Par exemple, si je concatène la chaîne "horses" 1000 fois à l'aide de strcat(), je devrai payer (1 + 2 + 3 + ... + 1000) * strlen("horses") = (1000*1001)/2 * 6 = 3003000

J'ai pensé à la manière non standard, de conserver un entier avec la longueur de la chaîne, puis d'envoyer à strcat() le pointeur vers la fin de la chaîne:

strcat(dest + dest_len, "string");

Dans ce cas, je ne paierai que 1000 * strlen("horses") = 1000 * 6 = 6000.

6000 est beaucoup plus bas que 3003000, il peut donc être très critique pour les performances si vous faites beaucoup de telles concaténations.

Existe-t-il un moyen plus standard de le faire, qui a l'air mieux que ma solution?

17
SomethingSomething

Joel Spolsky, dans son article Back to Basics , décrit le problème de la concaténation inefficace de chaînes avec strcat comme algorithme de Shlemiel le peintre (lire l'article, c'est plutôt bon) . Comme exemple de code inefficace, il donne cet exemple, qui tourne dans O (n2) temps:

char bigString[1000];     /* I never know how much to allocate... */
bigString[0] = '\0';
strcat(bigString,"John, ");
strcat(bigString,"Paul, ");
strcat(bigString,"George, ");
strcat(bigString,"Joel ");

Ce n'est pas vraiment un problème de marcher sur la première chaîne la première fois ; comme nous devons déjà parcourir la deuxième chaîne, le temps d'exécution de one strcat est linéaire dans la longueur du résultat. Plusieurs strcats sont problématiques cependant, car nous revoyons encore et encore les résultats précédemment concaténés. Il fournit cette alternative:

Comment réparons nous ça? Quelques programmeurs C intelligents ont implémenté leur propre mystrcat comme suit:

char* mystrcat( char* dest, char* src )
{
     while (*dest) dest++;
     while (*dest++ = *src++);
     return --dest;
}

Qu'avons-nous fait ici? À très peu de frais supplémentaires, nous renvoyons un pointeur vers la fin de la nouvelle chaîne plus longue. De cette façon, le code qui appelle cette fonction peut décider d'ajouter des éléments supplémentaires sans analyser à nouveau la chaîne:

char bigString[1000];     /* I never know how much to allocate... */
char *p = bigString;
bigString[0] = '\0';
p = mystrcat(p,"John, ");
p = mystrcat(p,"Paul, ");
p = mystrcat(p,"George, ");
p = mystrcat(p,"Joel ");

Ceci est, bien sûr, linéaire en performance, pas n-carré, donc il ne souffre pas de dégradation lorsque vous avez beaucoup de choses à concaténer.

Bien entendu, voici ce que vous pouvez faire si vous souhaitez utiliser des chaînes C standard. L'alternative que vous décrivez de mettre en cache la longueur de la chaîne et d'utiliser une fonction de concaténation spéciale (par exemple, en appelant strcat avec des arguments légèrement différents) est une sorte de variation sur les chaînes Pascal, que Joel a également mentionnée:

Les concepteurs de Pascal étaient conscients de ce problème et "l'ont corrigé" en enregistrant un nombre d'octets dans le premier octet de la chaîne. Celles-ci s'appellent Pascal Strings. Ils peuvent contenir des zéros et ne sont pas nuls. Etant donné qu'un octet ne peut stocker que des nombres compris entre 0 et 255, les chaînes Pascal ont une longueur limitée à 255 octets. Toutefois, comme elles ne sont pas terminées avec une valeur NULL, elles occupent la même quantité de mémoire que les chaînes ASCIZ. La grande chose à propos des chaînes Pascal est qu’il n’est jamais nécessaire de faire une boucle pour déterminer la longueur de votre chaîne. Trouver la longueur d'une chaîne en Pascal est une instruction d'assemblage au lieu d'une boucle entière. C'est monumentalement plus rapide.

Pendant longtemps, si vous vouliez mettre un littéral de chaîne Pascal dans votre code C, vous deviez écrire:

char* str = "\006Hello!";

Oui, vous deviez compter les octets à la main, vous-même, et les coder en dur dans le premier octet de votre chaîne. Les programmeurs paresseux le feraient et auraient des programmes lents

char* str = "*Hello!";
str[0] = strlen(str) - 1;
25
Joshua Taylor

Si vous voulez que ce soit facile, rapide, général, et safe, je suggère d’utiliser la fonction open_memstream() (elle fait partie de la norme POSIX-2008, elle n’a malheureusement pas été intégrée dans la norme C11, pensait). Cela fonctionne comme ceci:

D'abord, vous lui remettez l'adresse d'un pointeur et une taille

char* result = NULL;
size_t resultSize = 0;
FILE* stream = open_memstream(&result, &resultSize);

la valeur de retour est un flux de fichiers exactement comme si vous aviez utilisé fopen() pour ouvrir un fichier. En tant que tel, vous pouvez utiliser tout l’arsenal de fprintf() & co. pour diffuser tout ce que vous aimez dans votre mémoire tampon, qui est automatiquement allouée et gérée pour vous. Plus important encore, il garde également trace de la taille de la chaîne accumulée, de sorte qu'il n'est pas nécessaire de le ré-analyser pour en calculer la taille.

for(int i = 0; i < 1000000; i++) {
    fprintf(stream, "current number is %d, or 0x%x\n", i, i);
}

Enfin, vous fermez le flux, ce qui mettra à jour votre pointeur de résultat et la variable de taille afin de refléter la quantité réelle de données de chaîne écrites.

fclose(stream);
//Now you have a zero terminated C-string in result, and also its size in resultSize.
//You can do with it whatever you like.
//Just remember to free it afterwards:
free(result);
12
cmaster

Pour concaténer plusieurs chaînes, le code peut utiliser strlen() et memcpy(), deux fonctions souvent bien optimisées.

Avec cette approche, il est facile d’ajouter une limite size peu coûteuse.
Compte tenu des risques réels de débordement du tampon de destination, une taille limite est essentielle.

Durée d'exécution proportionnelle à la somme des longueurs de chaîne: O (len (S [0]) + len (S [1]) + len (S [2]) + ...)

char *strsncat(char *dest, size_t size, char * strs[], size_t n) {
  assert(size > 0);
  size--;
  char *p = dest;
  while (n-- > 0) {
    size_t len = strlen(*strs);
    if (len >= size) {
      len = size;
    }
    size -= len;
    memcpy(p, *strs, len);
    strs++;
    p += len;
  }
  *p = '\0';
  return dest;
}

void cat_test(void) {
  char dest[10];
  char *strs[]  = { "Red", "Green", "Blue" };
  printf("'%s'\n",strsncat(dest, sizeof dest, strs, sizeof strs/sizeof strs[0]));
  // 'RedGreenB'
}
2
chux

C'est une réponse tardive, mais je viens de rencontrer le même problème. Pour trouver un point de départ, j'ai décidé de relire les pages de manuel pour strcpy, strncpy, strlen, strnlen, strcat et strncat.

Cela m'a presque manqué, mais heureusement ... il y a un passage intéressant dans man strcpy sur mon système de développement (tronçon Debian). En le citant (formatage mine):

strlcpy()

Certains systèmes (BSD, Solaris et autres) offrent les fonctions suivantes:

size_t strlcpy(char *dest, const char *src, size_t size);

Cette fonction est similaire à strncpy(), mais elle copie au plus size-1 octets dans dest, ajoute toujours un octet nul final et ne remplit pas la cible avec des octets null (ultérieurs). Cette fonction résout certains des problèmes de strcpy() et strncpy(), mais l'appelant doit tout de même gérer la possibilité de perte de données si size est trop petit. La valeur de retour de la fonction est la longueur de src, ce qui permet de détecter facilement la troncature: si la valeur de retour est supérieure ou égale à size, une troncature s'est produite. Si la perte de données est importante, l'appelant doit vérifier les arguments avant l'appel ou tester la valeur de retour de la fonction.strlcpy() n'est pas présent dans glibc et n'est pas normalisé par POSIX, mais est disponible sous Linux via la bibliothèque libbsd.

Oui, vous lisez bien ce qui suit: La page de manuel d’une fonction glibc contient un indice sur une fonction non normalisée dans une autre bibliothèque qui fait mieux le travail. Cela pourrait prouver à quel point cette question est importante.

En passant, je ne comprendrai jamais pourquoi les concepteurs des fonctions str(n)cpy() n'ont pas choisi le nombre d'octets copiés ou un pointeur sur le nouveau end de dest comme valeur de retour. Retourner juste dest semble ridicule, car ces fonctions ne modifient pas ce paramètre. Dans tous les cas, l'appelant le sait toujours quand la fonction est revenue et, par conséquent, ce choix n'a pas de sens. Ai-je manqué quelque chose?

Jusqu'à ce que je connaisse strlcpy(), j'ai principalement utilisé mes propres fonctions de concaténation de chaînes, comme l'a montré @Joshua Taylor dans sa réponse. Cette idée a ses propres problèmes, cependant:

Analyser/copier des chaînes octet par octet peut s'avérer très inefficace. Selon le processeur cible, nous devrions utiliser les registres 32 bits, voire 64 bits, et copier plusieurs octets à la fois. Bien sûr, cela rend la fonction plus compliquée puisqu'il faut vérifier s'il reste suffisamment d'octets à copier, et si ce n'est pas le cas, utiliser la taille de registre immédiatement inférieure. Pour améliorer davantage les performances, nous devrions utiliser le code d'assemblage pour implémenter notre fonction.

D'après ce que je sais, des bibliothèques comme glibc et libbsd l'ont implémenté de cette manière. Il serait donc préférable d’utiliser l’implémentation libbsd. Je n'ai pas fait de mesures de performance, cependant.

1
Binarus

Supposons que vous ayez deux chaînes: s1 et s2 de longueur l1 et l2 La concaténation signifie que vous devez générer une nouvelle chaîne s3 de longueur l1+l2. La complexité temporelle de cette opération est O(l1+l2). De ce point de vue, strcat() semble être le meilleur choix.

Toutefois, si vous souhaitez indiquer l'état de concaténation de deux chaînes, il vous suffit d'enregistrer leurs pointeurs, à savoir O(1). Un exemple simple serait comme ceci:

typedef struct ConcatStr {
    char* str1;
    char* str2;
} ConcatStr;
ConcatStr myStrcat( char* str1, char* str2 )
{
    ConcatStr cstr;
    cstr.str1 = str1;
    cstr.str2 = str2;
}
1
rookiepig

J'utilise cette variante, qui remplace davantage le strcat, mais pas exactement:

char* mystrcat(char** dest, const char* src) {

    int i = 0;
    char cur;
    while(1) {
        cur = src[i];
        (*dest)[i] = cur;
        if(cur == 0) break;
        i++;
    }

    *dest += i;

    return *dest;
}

La valeur de retour n'est pas importante ici. Un tableau de caractères char str[32] ne contient pas de mémoire pour un pointeur réel sur les caractères (pour revenir à un pointeur), vous pouvez donc:

char str[32];
char* pStr = str; //storage for pointer
mystrcat(&pStr, "bla");
mystrcat(&pStr, "de");
mystrcat(&pStr, "bla\n");
printf(str);

ou

myfunction(char* pStr) {

    mystrcat(&pStr, "bla");
    mystrcat(&pStr, "de");
    mystrcat(&pStr, "bla\n");
}

char str[32];
myfunction(str);
printf(str);

car le stockage du pointeur est maintenant créé sur la pile pour myfunction ().

Une version à longueur limitée serait:

char* mystrcat(char** dest, const char* src, int max) {

    int i = 0;
    char cur;
    while(1) {
        if(i == max) {
            (*dest)[i] = 0;
            break;
        }
        cur = src[i];
        (*dest)[i] = cur;
        if(cur == 0) break;
        i++;
    }

    *dest += i;

    return *dest;
}
0
Rik Ruiter

Vérifie ça

https://john.nachtimwald.com/2017/02/26/efficient-c-string-builder/

Cela m'a aidé à copier un caractère ** dans le presse-papiers en un clin d'œil

    str_builder_t *sb;
     sb = str_builder_create();

                        int colcnt=0;
                        for (int i=0;i<nrF;i++)  // nrF = number of Fileds 
                    {
                            //strcat(DATA,sqlite_array[i]);
                     str_builder_add_str(sb, sqlite_array[i], 0); 
                            if (colcnt<nrofcolumns)  // my list view 
                                {
                            str_builder_add_str(sb, "\t", 0); 
                                colcnt++;

                            }
                                if (colcnt==nrofcolumns) 
                            {

                            str_builder_add_str(sb, "\n", 0); 
                                    colcnt=0;
                            }

                    }

    HANDLE  glob =GlobalAlloc(GMEM_FIXED,str_builder_len(sb)+1);
    memcpy(glob,str_builder_peek(sb),str_builder_len(sb)+1);
    OpenClipboard(NULL);
    EmptyClipboard();
    SetClipboardData(CF_TEXT,glob);
    CloseClipboard();   
0
Adrian