【问题标题】:Is there a C compiler that fails to compile this?是否有无法编译的 C 编译器?
【发布时间】:2008-11-29 23:56:27
【问题描述】:

我在我的分析器中闲逛了一段时间,试图弄清楚如何加速一个在日期解析方面遇到瓶颈的通用日志解析器,我尝试了各种算法来加快速度。

我尝试过的对我来说最快的东西也是迄今为止最易读的,但可能是非标准的 C。

这在GCCicc 和我非常古老且挑剔的 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;
}

【问题讨论】:

  • @JaredPar:我不认为 VS 是衡量可移植性的好方法。

标签: c


【解决方案1】:

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() 但编译器不合作。

【讨论】:

  • SPARC 是大端的吗?如果是这样,反转字符(或循环顺序)应该可以解决它。
  • 我对 GCC 和 Sun C 之间的区别非常感兴趣。尽管这充其量只是实现定义的行为,但通常情况下,GCC 可以很好地模拟本机编译器。不过,我不认为它是一个错误 - 在任何一个编译器中都没有。它只是“特定于实现”的行为和 AOK。
  • 另外,我尝试了:“/naJ”想要 -1 并返回“测试失败”,函数返回 0。因此,4 字节字逆序让测试通过。但它几乎不能解决问题 - parseMonth() 代码跨平台不可靠。
  • @Jonathan - 同意。人们已经发布了一些更快的解决方案,或者与我的问题一样漂亮的解决方案。我没有任何借口留下这段代码,就像我认为的那样漂亮,当我可能遇到真正的编译器时会失败。
  • @Dustin:查看我的修订版,它提供了一个可移植的解决方案。
【解决方案2】:
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
}

可读性稍差,验证性也不强,但也许更快,不是吗?

【讨论】:

  • 最后一行的错字('a' ? 0 -- 不是 1),但是是的,这在我的机器上速度略快两倍。在性能很重要的地方,你赢了。不过,我想知道这在哪里不够可读。
  • @Dustin,如果你添加 cmets 来解释它,它是可读的 - 不会影响运行速度,它将使它和你的一样可读,以便下一个看它的人。我会确保首先列出 31 天的月份,因为这会带来一点额外的速度(统计上)。
  • 假设case语句按照给定的顺序编译,即。
  • 我不相信“Zax”应该被接受为一月;也不应该接受 Xxx 作为七月。
  • 正如我所说 - 它没有验证。如果您想要验证,那么请注意这些行正在寻找。
【解决方案3】:

您只是在计算这四个字符的哈希值。为什么不预定义一些以相同方式计算散列的整数常量并使用它们呢?具有相同的可读性,并且您不依赖于编译器的任何实现特定特性。

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

【讨论】:

  • 我确实喜欢这个美学。我认为它看起来和我所做的一样好,但可以使其工作更加一致,并且不会偶尔出现编译器警告。谢谢。
  • 使用宏; #define MON_HASH(s) ((((s)[0]) &amp; 0xff &lt;&lt; 24) | (((s)[1]) &amp; 0xff &lt;&lt; 16) | (((s)[2]) &amp; 0xff &lt;&lt; 8) | (((s)[3]) &amp; 0xff)) ... switch (MON_HASH(input)) { ... case MON_HASH("Jan/")注意 使用 gperf 之类的东西来生成 8 位(或更短)哈希(Vilx- 最接近 gperf 所做的)将比 int 更有效。哈希”(int 上的 switch 不会使用跳转表实现,而 char__int8 上的 switch 可能会。)
【解决方案4】:

我只知道 C 标准对此有何评论 (C99):

整数字符的值 包含一个以上的常数 字符(例如,'ab'),或包含 一个字符或转义序列 不映射到单字节 执行字符,是 实现定义。如果一个整数 字符常量包含单个 字符或转义序列,其 值是当一个 类型为 char 的对象,其值为 单个字符或转义的 序列被转换为 int 类型。

(6.4.4.4/10 取自草稿)

所以它是实现定义的。这意味着它不能保证它在任何地方都一样,但行为必须由实现记录。例如,如果 int 在特定实现中只有 16 位宽,那么 'Jan/' 就不能再像您想要的那样表示(char 必须至少为 8 位,而字符文字始终是 @ 类型987654324@).

【讨论】:

  • 理论上是错误的,我明白。我想知道是否有任何 C 编译器在实践中不起作用。这完全是出于好奇,因为即使在一个古老的编译器上,我找到的一半以上的开源内容都失败了,它也对我有用。
  • @Dustin,尝试为 PPC(非 Intel Mac OS X)编译。我认为这是大端。
  • @strager 做了...我在我的 G3 和 SGI 上运行了这个(有点双向,但在大端运行 IRIX)。
  • @Dustin,我有一个旧的 8051 编译器无法编译它 - 它只有 16 位整数,并且它在 'xxxx' 常量上作呕(它也会在运行时因移位而失败 -或代码)。事实上,它也会对 'xx' 常量感到困惑。
  • 达斯汀。您可能不会通过编译器,而是通过他们的后端来计算。我可以通过 GCC 后端更改为 int 16 位宽,然后 gcc-eco32 会错误编译它。
【解决方案5】:
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;

【讨论】:

  • 我也做了那个。它肯定更小,但并不快,因为解析 12 月的日期比 1 月的日期要长得多。我有一个可以调整干草堆以利用按时间顺序排列的输入。更复杂,但并不是真的更快。
  • 令人惊讶的是 strstr() 在大多数 PC 上应该编译成几乎一条机器指令
  • @Dustin,我在我 5 岁的电脑上使用您的原始方法和 Scott 的方法运行了一些没有优化的基准测试。 Scott 在 1.6 秒内找到一千万次,在 8.3 秒内找到十二月,您的程序在 2.6 秒内找到一月和十二月。因此,虽然明显更清晰,但斯科特的方法有点慢......
  • 在某些情况下。我的问题是:你到底在做什么,只能每秒调用一百万次导致瓶颈?
  • @Robert,“在没有优化的情况下运行了一些基准测试”
【解决方案6】:

至少有 3 件事使该程序无法移植:

  1. 多字符常量是实现定义的,因此不同的编译器可能会以不同的方式处理它们。
  2. 一个字节可以超过 8 位,有很多硬件的最小可寻址内存单元是 16 位甚至 32 位,例如,您经常在 DSP 中发现这一点。如果一个字节超过 8 位,那么 char 也将如此,因为根据定义,char 是一个字节长;您的程序将无法在此类系统上正常运行。
  3. 最后,包括嵌入式设备和遗留机器在内的许多机器上 int 仅为 16 位(这是 int 允许的最小大小),您的程序也会在这些机器上失败。

【讨论】:

  • 我的 64 位处理器有 32 位 int,它工作正常。也许你的意思是小于?
  • 真的还是 BYTE 的东西? AFAIK 字节定义为 8 位
  • @JaredPar:C 中的一个字节至少有 8 位,但可能有更多。今天有很多系统(也有新的!)字节大于 8 位。
  • 好的,您使用的是 C 定义(出了名的模糊)。现在我已经明白了,我在我的代码中使用显式大小的类型来避免混淆其他人。例如:__int16、__int32 等 ...
  • @JaredPar:有很多硬件的最小可寻址内存单元是 16 位或 32 位,这不仅仅是 C 定义的事情。在这样的系统上,char 将是 16 或 32 位,而 int 可能只有 1 个字节。世界不是 Windows PC ;)
【解决方案7】:

National Instrument's CVI 8.5 for Windows 编译器在您的原始代码上失败并出现多个警告:

  Warning: Excess characters in multibyte character literal ignored.

和表格的错误:

  Duplicate case label '77'.

乔纳森的代码成功了。

【讨论】:

    【解决方案8】:

    我收到警告,但没有错误 (gcc)。似乎编译和运行良好。 但可能不适用于大端系统!

    不过,我不建议使用这种方法。也许您可以通过异或而不是或移位来创建单个字节。然后在一个字节上使用 case 语句(或者,更快,使用前 N 位的 LUT)。

    【讨论】:

    • 这实际上是一个完美的哈希。对于这个应用程序来说,这并不重要,但完美哈希的缺点是它可能会因输入错误而返回错误的答案。
    • @Dustin,那么一旦你有可能的答案来检查它,你就可以做一个 strcmp 。为此使用 LUT。容易。
    • 我早期的一个尝试是编写一个程序,该程序将一组单词作为输入,并在执行 strcmp 之前生成一个切换字母的分层 case 语句。例如:pastebin.com/f30e09462(可选地进一步分解 Js)。
    • @Dustin,您或许可以将您的交换机拆分为更多交换机。这应该比调用 memcmp 更快。如果你不想这样做,你可以利用你已经知道第一个字符的事实,并且不需要比较它。
    • @strager 我写了这个东西来为我构建它:github.com/dustin/snippets/tree/master/python/misc/gensearch.py
    【解决方案9】:

    四字符常量等价于特定的 32 位整数这一事实是一个非标准功能,通常出现在 MS Windows 和 Mac 计算机(以及 PalmOS、AFAICR)的编译器上。

    在这些系统中,四个字符串通常用作标识数据文件块的标记,或用作应用程序/数据类型标识符(例如“APPL”)。

    这对开发人员来说很方便,他们可以将这样的字符串存储到各种数据结构中,而不必担心零字节终止、指针等。

    【讨论】:

      【解决方案10】:

      Comeau compiler

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

      【讨论】:

      • 感谢编译器指针。我不担心编译器会抱怨潜在的可移植性问题,只是编译器会产生行为不同的代码。
      【解决方案11】:

      抛开机器字长问题不谈,您的编译器可能会将 input[i] 提升为负整数,这只会使用 or 操作设置 inputInt 的高位,因此我建议您明确说明 char 变量的符号性。

      但由于在美国,没有人关心第 8 位,这对你来说可能不是问题。

      【讨论】:

      • IBM 非常关心第 8 位,他们甚至使用 Unicode :-)
      【解决方案12】:

      我很想看到显示 this 是您最重要的瓶颈的分析,但无论如何,如果您要提取这样的东西,请使用联合而不是 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 $ 
      

      【讨论】:

      • 我做了类似的事情,只是将 char* 转换为 int*,然后从其中读取。这就是我遇到字节序问题的地方。
      • 这些东西都不会受到字节序等问题的影响,而且这很容易受到混合大小写的人的影响。
      【解决方案13】:

      正如其他人所提到的,该代码会引发一堆警告,并且可能不是字节序安全的。

      您原来的日期解析器也是手写的吗?你试过 strptime(3) 吗?

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-11-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-09-29
        相关资源
        最近更新 更多