Y at-il un compilateur C qui ne parvient pas à comstackr cela?

Pendant quelques temps, je me suis tenu dans mon profileur pour essayer de trouver un moyen d’accélérer un parsingur de journal commun goulot d’étranglement lors de l’parsing de la date et j’ai essayé divers algorithmes pour accélérer les choses.

La chose que j’ai essayée qui était la plus rapide pour moi était aussi de loin la plus lisible, mais potentiellement non standard.

Cela a très bien fonctionné avec GCC , la CIO et mon très vieux et difficile compilateur SGI. Comme il s’agit d’une optimisation assez lisible, où ne fait-il pas ce que je veux?

static int parseMonth(const char *input) { int rv=-1; int inputInt=0; int i=0; for(i=0; i<4 && input[i]; i++) { inputInt = (inputInt << 8) | input[i]; } switch(inputInt) { case 'Jan/': rv=0; break; case 'Feb/': rv=1; break; case 'Mar/': rv=2; break; case 'Apr/': rv=3; break; case 'May/': rv=4; break; case 'Jun/': rv=5; break; case 'Jul/': rv=6; break; case 'Aug/': rv=7; break; case 'Sep/': rv=8; break; case 'Oct/': rv=9; break; case 'Nov/': rv=10; break; case 'Dec/': rv=11; break; } return rv; } 

Compilateur Solaris 10 – SPARC – SUN.

Code de test:

 #include  static int parseMonth(const char *input) { int rv=-1; int inputInt=0; int i=0; for(i=0; i<4 && input[i]; i++) { inputInt = (inputInt << 8) | input[i]; } switch(inputInt) { case 'Jan/': rv=0; break; case 'Feb/': rv=1; break; case 'Mar/': rv=2; break; case 'Apr/': rv=3; break; case 'May/': rv=4; break; case 'Jun/': rv=5; break; case 'Jul/': rv=6; break; case 'Aug/': rv=7; break; case 'Sep/': rv=8; break; case 'Oct/': rv=9; break; case 'Nov/': rv=10; break; case 'Dec/': rv=11; break; } return rv; } static const struct { char *data; int result; } test_case[] = { { "Jan/", 0 }, { "Feb/", 1 }, { "Mar/", 2 }, { "Apr/", 3 }, { "May/", 4 }, { "Jun/", 5 }, { "Jul/", 6 }, { "Aug/", 7 }, { "Sep/", 8 }, { "Oct/", 9 }, { "Nov/", 10 }, { "Dec/", 11 }, { "aJ/n", -1 }, }; #define DIM(x) (sizeof(x)/sizeof(*(x))) int main(void) { size_t i; int result; for (i = 0; i < DIM(test_case); i++) { result = parseMonth(test_case[i].data); if (result != test_case[i].result) printf("!! FAIL !! %s (got %d, wanted %d)\n", test_case[i].data, result, test_case[i].result); } return(0); } 

Résultats (GCC 3.4.2 et Sun):

 $ gcc -O xx.c -o xx xx.c:14:14: warning: multi-character character constant xx.c:15:14: warning: multi-character character constant xx.c:16:14: warning: multi-character character constant xx.c:17:14: warning: multi-character character constant xx.c:18:14: warning: multi-character character constant xx.c:19:14: warning: multi-character character constant xx.c:20:14: warning: multi-character character constant xx.c:21:14: warning: multi-character character constant xx.c:22:14: warning: multi-character character constant xx.c:23:14: warning: multi-character character constant xx.c:24:14: warning: multi-character character constant xx.c:25:14: warning: multi-character character constant $ ./xx $ cc -o xx xx.c $ ./xx !! FAIL !! Jan/ (got -1, wanted 0) !! FAIL !! Feb/ (got -1, wanted 1) !! FAIL !! Mar/ (got -1, wanted 2) !! FAIL !! Apr/ (got -1, wanted 3) !! FAIL !! May/ (got -1, wanted 4) !! FAIL !! Jun/ (got -1, wanted 5) !! FAIL !! Jul/ (got -1, wanted 6) !! FAIL !! Aug/ (got -1, wanted 7) !! FAIL !! Sep/ (got -1, wanted 8) !! FAIL !! Oct/ (got -1, wanted 9) !! FAIL !! Nov/ (got -1, wanted 10) !! FAIL !! Dec/ (got -1, wanted 11) $ 

Notez que le dernier cas de test est toujours passé, c’est-à-dire qu’il a généré -1.

Voici une version révisée - plus détaillée - de parseMonth () qui fonctionne de la même manière sous les compilateurs GCC et Sun C:

 #include  /* MONTH_CODE("Jan/") does not reduce to an integer constant */ #define MONTH_CODE(x) ((((((x[0]<<8)|x[1])<<8)|x[2])<<8)|x[3]) #define MONTH_JAN (((((('J'<<8)|'a')<<8)|'n')<<8)|'/') #define MONTH_FEB (((((('F'<<8)|'e')<<8)|'b')<<8)|'/') #define MONTH_MAR (((((('M'<<8)|'a')<<8)|'r')<<8)|'/') #define MONTH_APR (((((('A'<<8)|'p')<<8)|'r')<<8)|'/') #define MONTH_MAY (((((('M'<<8)|'a')<<8)|'y')<<8)|'/') #define MONTH_JUN (((((('J'<<8)|'u')<<8)|'n')<<8)|'/') #define MONTH_JUL (((((('J'<<8)|'u')<<8)|'l')<<8)|'/') #define MONTH_AUG (((((('A'<<8)|'u')<<8)|'g')<<8)|'/') #define MONTH_SEP (((((('S'<<8)|'e')<<8)|'p')<<8)|'/') #define MONTH_OCT (((((('O'<<8)|'c')<<8)|'t')<<8)|'/') #define MONTH_NOV (((((('N'<<8)|'o')<<8)|'v')<<8)|'/') #define MONTH_DEC (((((('D'<<8)|'e')<<8)|'c')<<8)|'/') static int parseMonth(const char *input) { int rv=-1; int inputInt=0; int i=0; for(i=0; i<4 && input[i]; i++) { inputInt = (inputInt << 8) | input[i]; } switch(inputInt) { case MONTH_JAN: rv=0; break; case MONTH_FEB: rv=1; break; case MONTH_MAR: rv=2; break; case MONTH_APR: rv=3; break; case MONTH_MAY: rv=4; break; case MONTH_JUN: rv=5; break; case MONTH_JUL: rv=6; break; case MONTH_AUG: rv=7; break; case MONTH_SEP: rv=8; break; case MONTH_OCT: rv=9; break; case MONTH_NOV: rv=10; break; case MONTH_DEC: rv=11; break; } return rv; } static const struct { char *data; int result; } test_case[] = { { "Jan/", 0 }, { "Feb/", 1 }, { "Mar/", 2 }, { "Apr/", 3 }, { "May/", 4 }, { "Jun/", 5 }, { "Jul/", 6 }, { "Aug/", 7 }, { "Sep/", 8 }, { "Oct/", 9 }, { "Nov/", 10 }, { "Dec/", 11 }, { "aJ/n", -1 }, { "/naJ", -1 }, }; #define DIM(x) (sizeof(x)/sizeof(*(x))) int main(void) { size_t i; int result; for (i = 0; i < DIM(test_case); i++) { result = parseMonth(test_case[i].data); if (result != test_case[i].result) printf("!! FAIL !! %s (got %d, wanted %d)\n", test_case[i].data, result, test_case[i].result); } return(0); } 

Je voulais utiliser MONTH_CODE () mais les compilateurs n'ont pas coopéré.

 if ( !input[0] || !input[1] || !input[2] || input[3] != '/' ) return -1; switch ( input[0] ) { case 'F': return 1; // Feb case 'S': return 8; // Sep case 'O': return 9; // Oct case 'N': return 10; // Nov case 'D': return 11; // Dec; case 'A': return input[1] == 'p' ? 3 : 7; // Apr, Aug case 'M': return input[2] == 'r' ? 2 : 4; // Mar, May default: return input[1] == 'a' ? 0 : (input[2] == 'n' ? 5 : 6); // Jan, Jun, Jul } 

Un peu moins lisible et pas tellement valide, mais peut-être même plus vite, non?

Vous ne faites que calculer un hachage de ces quatre personnages. Pourquoi ne pas prédéfinir des constantes de nombre entier qui calculent le hachage de la même manière et les utilisent? Même lisibilité et vous ne dépendez d’aucune idiosyncrasie spécifique du compilateur pour l’implémentation.

 uint32_t MONTH_JAN = 'J' << 24 + 'a' << 16 + 'n' << 8 + '/'; uint32_t MONTH_FEB = 'F' << 24 + 'e' << 16 + 'b' << 8 + '/'; ... static uint32_t parseMonth(const char *input) { uint32_t rv=-1; uint32_t inputInt=0; int i=0; for(i=0; i<4 && input[i]; i++) { inputInt = (inputInt << 8) | (input[i] & 0x7f); // clear top bit } switch(inputInt) { case MONTH_JAN: rv=0; break; case MONTH_FEB: rv=1; break; ... } return rv; } 

Je sais seulement ce que dit la norme C à ce sujet (C99):

La valeur d’une constante de caractère entier contenant plusieurs caractères (par exemple, ‘ab’), ou contenant un caractère ou une séquence d’échappement ne mappant pas sur un caractère d’exécution codé sur un octet, est définie par l’implémentation. Si une constante de caractère entier contient un seul caractère ou une séquence d’échappement, sa valeur est celle obtenue lorsqu’un object de type char, dont la valeur correspond à celle du caractère ou de la séquence d’échappement, est converti en type int.

(6.4.4.4/10 extrait d’un projet)

Donc, c’est la mise en œuvre définie. Cela signifie qu’il n’est pas garanti que cela fonctionne de la même manière partout, mais le comportement doit être documenté par la mise en œuvre. Par exemple, si int n’a qu’une largeur de 16 bits dans une implémentation particulière, alors 'Jan/' ne peut plus être représenté comme vous le souhaitez ( char doit avoir au moins 8 bits, tandis qu’un caractère littéral est toujours du type int ).

 char *months = "Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec/"; char *p = strnstr(months, input, 4); return p ? (p - months) / 4 : -1; 

Le compilateur CVI 8.5 pour Windows de National Instrument échoue sur votre code d’origine avec plusieurs avertissements:

  Warning: Excess characters in multibyte character literal ignored. 

et erreurs de la forme:

  Duplicate case label '77'. 

Il réussit sur le code de Jonathan.

Il y a au moins 3 choses qui empêchent ce programme d’être portable:

  1. Les constantes à plusieurs caractères étant définies par la mise en œuvre, différents compilateurs peuvent les traiter différemment.
  2. Un octet peut contenir plus de 8 bits. Il existe de nombreux matériels dans lesquels la plus petite unité de mémoire adressable est de 16, voire 32 bits. Vous le trouverez souvent dans les DSP, par exemple. Si un octet est supérieur à 8 bits, alors char sera, car char est par définition long d’un octet; votre programme ne fonctionnera pas correctement sur de tels systèmes.
  3. Enfin, il existe de nombreuses machines pour lesquelles int ne mesure que 16 bits (ce qui correspond à la plus petite taille autorisée pour int), y compris les périphériques intégrés et les machines héritées, votre programme échouera également sur ces machines.

Je reçois des avertissements, mais pas d’erreurs (gcc). Semble comstackr et fonctionner correctement. Peut ne pas fonctionner pour les systèmes big-endian, cependant!

Je ne suggérerais pas cette méthode, cependant. Peut-être que vous pouvez xor au lieu de ou-shift, pour créer un seul octet. Utilisez ensuite l’instruction case sur un octet (ou, plus rapidement, utilisez une table de conversion des N premiers bits).

Compilateur Comeau

 Comeau C/C++ 4.3.10.1 (Oct 6 2008 11:28:09) for ONLINE_EVALUATION_BETA2 Copyright 1988-2008 Comeau Computing. All rights reserved. MODE:ssortingct errors C99 "ComeauTest.c", line 11: warning: multicharacter character literal (potential portability problem) case 'Jan/': rv=0; break; ^ "ComeauTest.c", line 12: warning: multicharacter character literal (potential portability problem) case 'Feb/': rv=1; break; ^ "ComeauTest.c", line 13: warning: multicharacter character literal (potential portability problem) case 'Mar/': rv=2; break; ^ "ComeauTest.c", line 14: warning: multicharacter character literal (potential portability problem) case 'Apr/': rv=3; break; ^ "ComeauTest.c", line 15: warning: multicharacter character literal (potential portability problem) case 'May/': rv=4; break; ^ "ComeauTest.c", line 16: warning: multicharacter character literal (potential portability problem) case 'Jun/': rv=5; break; ^ "ComeauTest.c", line 17: warning: multicharacter character literal (potential portability problem) case 'Jul/': rv=6; break; ^ "ComeauTest.c", line 18: warning: multicharacter character literal (potential portability problem) case 'Aug/': rv=7; break; ^ "ComeauTest.c", line 19: warning: multicharacter character literal (potential portability problem) case 'Sep/': rv=8; break; ^ "ComeauTest.c", line 20: warning: multicharacter character literal (potential portability problem) case 'Oct/': rv=9; break; ^ "ComeauTest.c", line 21: warning: multicharacter character literal (potential portability problem) case 'Nov/': rv=10; break; ^ "ComeauTest.c", line 22: warning: multicharacter character literal (potential portability problem) case 'Dec/': rv=11; break; ^ "ComeauTest.c", line 1: warning: function "parseMonth" was declared but never referenced static int parseMonth(const char *input) { ^ 

Le fait qu’une constante de quatre caractères soit équivalente à un entier 32 bits particulier est une fonctionnalité non standard souvent vue sur les compilateurs pour ordinateurs MS Windows et Mac (et PalmOS, AFAICR).

Sur ces systèmes, une chaîne de quatre caractères est couramment utilisée comme balise pour identifier des fragments de fichiers de données ou comme identificateur d’application / type de données (par exemple, “APPL”).

Il est donc pratique pour le développeur de pouvoir stocker une telle chaîne dans diverses structures de données sans se soucier de la terminaison de zéro octet, des pointeurs, etc.

Mis à part les problèmes de taille des mots de la machine, votre compilateur peut promouvoir l’entrée [i] en un entier négatif qui ne fera que définir les bits supérieurs de inputInt avec ou operation, je vous suggère donc d’être explicite sur la signature des variables char.

Mais comme aux États-Unis, personne ne se soucie du 8ème bit, c’est probablement un problème qui ne vous concerne pas.

J’adorerais voir le profilage qui montre que c’est votre principal goulot d’étranglement, mais dans tous les cas, si vous voulez tirer quelque chose comme ça, utilisez une union au lieu de 50 instructions en boucle. Voici un petit exemple de programme, je vous laisse le soin de l’intégrer à votre programme.

 /* union -- demonstrate union for characters */ #include  union c4_i { char c4[5]; int i ; } ; union c4_i ex; int main (){ ex.c4[0] = 'a'; ex.c4[1] = 'b'; ex.c4[2] = 'c'; ex.c4[3] = 'd'; ex.c4[4] = '\0'; printf("%s 0x%08x\n", ex.c4, ex.i ); return 0; } 

Voici un exemple de sortie:

 bash $ ./union abcd 0x64636261 bash $ 

Comme mentionné par d’autres, ce code émet une série d’avertissements et n’est probablement pas endian-safe.

Votre parsingur de date original a-t-il également été écrit à la main? Avez-vous essayé strptime (3)?