快速strlen功能vs别名规则

时间:2016-06-28 15:26:10

标签: c++ c type-conversion strlen strict-aliasing

我找到了this"快速strlen功能"实现:

// for x86 only
size_t my_strlen(const char *s) {
    size_t len = 0;
    for(;;) {
        unsigned x = *(unsigned*)s;
        if((x & 0xFF) == 0) return len;
        if((x & 0xFF00) == 0) return len + 1;
        if((x & 0xFF0000) == 0) return len + 2;
        if((x & 0xFF000000) == 0) return len + 3;
        s += 4, len += 4;
    }
}

这里使用的优化技术显然很简单:通过自然CPU字读取内存(代码是旧的并假定为x32 CPU),而不是简单的字节。

但是这段代码违反了别名规则,因此会导致未定义的行为,编译器可以自由地优化这些行为(那些使代码更快,但是更多的代码)。

我现在也看到它不可移植,因为它与little-endian字节序有关。

或者我可能完全错了,上面的代码是正确的?这对C来说是否正确?对于C ++?

3 个答案:

答案 0 :(得分:5)

这只是非常糟糕的代码。甚至代码的作者都警告说:

  • 如果不可读的内存页面位于字符串结尾之后,此函数将崩溃。 防止这种情况的最简单方法是在字符串末尾分配3个额外字节。

  • dword可能是未对齐的,但x86架构允许访问未对齐的数据。对于小字符串,对齐将花费更多时间而不是未对齐读取的惩罚

  • 代码不可移植:如果使用64位处理器,则必须添加另外4个条件。对于大端架构,条件的顺序应该颠倒过来。

即使这没有打破别名规则,编码器使my_strlen工作的负担也是完全没有道理的。已多次声明strlen已经超出了普通编码员可以完成的任何事情。

但是应该为C ++做一个额外的陈述:Bjarne Stroustrup, the creator of C++,中的last page of chapter 4 in his book:“C ++编程语言”说:

  

首选strings超过C风格的字符串

你会发现string的大小比找到C-String的大小要高得多。

修改

your comment中,您说您正在使用StaticallyBufferedString声称要解决string的“池化内存模型”,这会导致:

  • 多线程上下文中不必要的堆锁
  • 来自实时大小控制的碎片

我想建议C ++ 17的string_view与所有C ++ 17一样,构建时考虑了多线程。它提供了由{x}支持的stringconstexpr友好C字符串的功能。您甚至可以通过namespace experimental快速了解它:http://en.cppreference.com/w/cpp/experimental/basic_string_view与您在StaticallyBufferedStrings上的时间不同,您获得的知识将非常便携,适用于您未来的任何C ++工作!

答案 1 :(得分:4)

这是一个坏主意,会让你的表现更糟。现代编译器提供的标准strlen功能已经过高度优化,并且可以比上面做得更好。例如,在启用了SSE的CPU上(即几乎所有的CPU),它已经使用SSE / AVX指令对空终止符进行矢量化搜索,并且如上所述,将一次考虑多于4个字节,而且比较较少 - 相关指令,以及可能误预测的分支较少。

答案 2 :(得分:1)

写入的函数既不是最佳的也不是可移植的。话虽如此,C89的作者还是包含了一些写得很糟糕的规则,如果严格解释这些规则,C89的语言就会比许多平台上存在的早期C语言强得多。规则的目的是避免要求C编译器给出如下代码:

float *fp;
int i;

int foo(void)
{
  i++;
  *fp=1.0f;
  return i;
}

从悲观地假设写入*fp可能会影响i ,尽管完全没有任何可能表明类型int可能会受到影响的内容。在编写C89时,使用类型惩罚用于各种目的(包括分块优化)的代码很普遍,但是大多数此类代码都会向编译器明确指出混叠将要发生。通常,如果一个对象将被两个" normal"之间的外来类型指针修改。访问时,在两次正常访问之间会发生以下一种或两种情况:

  1. 将使用对象的地址。

  2. 指针将从对象的类型转换为另一种类型。

  3. 除了使用指针访问对象的明显情况之外 它的精确类型,标准主要是识别它不会的情况 很明显,编译器应该假设混叠是可能的(例如,之间 " int"和#34; unsigned *"或任何东西和指针之间的指针 类型" char *")。鉴于理由,我认为作者打算这样做 专注于确保编译器编写者处理那里的情况 没有理由期待别名,但没有想到他们需要 告诉我如何识别明显可能的情况。

    分块优化对编译器来说是安全的,它们会识别出 地址和转换运算符意味着可能存在交叉类型别名, 只要执行所有使用由强制转换产生的指针 在使用非投射指针进行下一次访问之前 - 这是大多数要求 分块代码通常会遇到。不幸的是,没有标准 for" sane-compiler C"和gcc使用的事实是作者的 标准并没有要求编译器处理明显的别名情况 理由忽视它们。

    然而,分块优化的性能优势可能会超过 -fno-strict-aliasing的性能成本,特别是在代码使用时 适当时restrict个限定词。主要有一些情况 涉及全局变量,其中restrict不足以启用 有用的优化;那些可以通过限制的混叠模式来处理 分析静态或自动持续时间的对象(如对象中的对象) 理由(例如)但gcc没有提供这样的模式。

    顺便说一下,我不确定现代x86处理器上的指令时序是什么样的,但是在某些ARM变体上,编译器有可能从以下内容生成最佳的长字符串代码:

    uint32_t x01000000 = 0x01000000;
    uint64_t *search(uint64_t *p)
    {
      uint64_t n;
      uint32_t a,b;
      uint32_t v = x01000000; // See note
      do
      {
        n=*p++;
        a=(uint32_t)n;
        b=n>>32;
        if (a >= v  || (a << 8) >= v || (a << 16) >= v || (a << 24) >= v ||
            b >= v  || (b << 8) >= v || (b << 16) >= v || (b << 24) >= v) return p;
      } while(1);      
    }
    

    确定哪个比较导致断开循环会花费额外的时间,但是将这些考虑因素排除在循环之外可能会使循环本身更有效。

    许多ARM处理器都有一条指令,可以将移位值与寄存器进行比较;编译器有时需要一些帮助才能意识到0x01000000应保存在寄存器中(存在与常量比较的指令,但是 不包括&#34;免费&#34;被比较的寄存器的移位),但在帮助下他们可以找到与移位比较。我还没有找到一种方法来说服编译器为ARM7-TDMI生成最佳代码,这相当于:

    search:
        mov    r1,#0x010000000
    lp:
        ldrmia r0,{r2,r3}
        cmp    r1,r2
        cmplt  r1,r2,asl #8
        cmplt  r1,r2,asl #16
        cmplt  r1,r2,asl #24
        cmplt  r1,r3
        cmplt  r1,r3,asl #8
        cmplt  r1,r3,asl #16
        cmplt  r1,r3,asl #24
        blt    lp
        bx     lr
    

    每八个字节需要15个周期;它可以适应每十六个字节需要25个周期。一个单独处理8个字节的循环需要42个周期;展开到16个字节将是82个周期。我已经看到编译器为基于uint64_t的代码生成的最佳循环将是8个字节的22个周期 - 几乎是最佳代码的一半,但仍然是使用字节的版本的两倍。