Есть ли компилятор C, который не может это скомпилировать?

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

То, что я попробовал, было самым быстрым для меня, и, безусловно, самым читаемым, но потенциально нестандартным C.

Это хорошо работало в GCC, icc и моем действительно старом и требовательном компиляторе SGI. Поскольку это вполне читаемая оптимизация, где она не делает то, что я хочу?

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

13 ответов

Решение

Solaris 10 - SPARC - SUN Компилятор.

Тестовый код:

#include <stdio.h>

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

Результаты (GCC 3.4.2 и 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)
$

Обратите внимание, что последний тестовый пример все же пройден - то есть он сгенерировал -1.

Вот пересмотренная - более подробная - версия parseMonth(), которая работает одинаково и для GCC, и для компилятора Sun C:

#include <stdio.h>

/* 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);
}

Я хотел использовать MONTH_CODE(), но компиляторы не сотрудничали.

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
}

Чуть менее читабельно и не так много проверяющего, но, возможно, даже быстрее, нет?

Вы просто вычисляете хэш этих четырех символов. Почему бы не предопределить некоторые целочисленные константы, которые вычисляют хеш одинаково, и использовать их? Та же читаемость, и вы не зависите от каких-либо специфических особенностей реализации компилятора.

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

Я знаю только то, что Стандарт C говорит об этом (C99):

Значение целочисленной символьной константы, содержащей более одного символа (например, 'ab'), или содержащей символ или escape-последовательность, которая не отображается на однобайтовый символ выполнения, определяется реализацией. Если целочисленная символьная константа содержит один символ или escape-последовательность, ее значение является тем, которое получается, когда объект с типом char, значение которого равно значению одиночного символа или escape-последовательности, преобразуется в тип int.

(6.4.4.4/10 взято из черновика)

Так что реализация определена. Это означает, что не гарантируется, что он везде работает одинаково, но поведение должно быть задокументировано реализацией. Например, если int только 16 бит в конкретной реализации, то 'Jan/' больше не может быть представлен, как вы намерены (char должно быть не менее 8 бит, в то время как символьный литерал всегда имеет тип 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;

Есть по крайней мере 3 вещи, которые мешают этой программе быть переносимой:

  1. Многосимвольные константы определяются реализацией, поэтому разные компиляторы могут обрабатывать их по-разному.
  2. Байт может быть больше 8 бит, есть много оборудования, где наименьшая адресуемая единица памяти составляет 16 или даже 32 бита, это часто встречается, например, в DSP. Если байт больше 8 бит, то так будет char поскольку char по определению длиной в один байт; Ваша программа не будет работать должным образом в таких системах.
  3. Наконец, есть много машин, где int только 16-битный (это наименьший размер, допустимый для int), включая встроенные устройства и устаревшие машины, ваша программа также будет работать на этих машинах.

CVI 8.5 для Windows компилятора National Instrument завершается с ошибкой в ​​исходном коде с несколькими предупреждениями:

  Warning: Excess characters in multibyte character literal ignored.

и ошибки формы:

  Duplicate case label '77'.

Это успешно по коду Джонатана.

Я получаю предупреждения, но без ошибок (GCC). Вроде компилируется и работает нормально. Может не работать для систем с прямым порядком байтов!

Я бы не предложил этот метод, хотя. Возможно, вы можете xor вместо or-shift, чтобы создать один байт. Затем используйте оператор case для байта (или, скорее, используйте LUT первых N битов).

Тот факт, что четырехсимвольная константа эквивалентна конкретному 32-разрядному целому числу, является нестандартной функцией, часто встречающейся в компиляторах для компьютеров под управлением Windows и Mac (и PalmOS, AFAICR).

В этих системах четырехсимвольная строка обычно используется в качестве тега для идентификации фрагментов файлов данных или в качестве идентификатора приложения / типа данных (например, "APPL").

Для разработчиков удобнее то, что они могут хранить такую ​​строку в различных структурах данных, не беспокоясь об окончании нулевого байта, указателях и т. Д.

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:strict 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) {
             ^

Если не принимать во внимание размер машинного слова, ваш компилятор может преобразовать input[i] в ​​отрицательное целое число, которое просто установит верхние биты inputInt с помощью или операции, поэтому я предлагаю вам четко указать на подпись переменных типа char.

Но поскольку в США никто не заботится о 8-м бите, это, вероятно, не проблема для вас.

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

Был ли ваш оригинальный парсер дат написан от руки? Вы пробовали strptime(3)?

Мне бы очень хотелось, чтобы профилирование показало, что это ваше самое существенное узкое место, но в любом случае, если вы собираетесь использовать что-то подобное, используйте объединение вместо 50 циклических и сдвиговых инструкций. Вот небольшой пример программы, я оставлю ее вам, чтобы она вписалась в вашу программу.

/* union -- demonstrate union for characters */

#include <stdio.h>

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

Вот пример вывода:

bash $ ./union
abcd 0x64636261
bash $ 
Другие вопросы по тегам