我找到了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 ++?
答案 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}支持的string
和constexpr
友好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"之间的外来类型指针修改。访问时,在两次正常访问之间会发生以下一种或两种情况:
将使用对象的地址。
指针将从对象的类型转换为另一种类型。
除了使用指针访问对象的明显情况之外 它的精确类型,标准主要是识别它不会的情况 很明显,编译器应该假设混叠是可能的(例如,之间 " 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个周期 - 几乎是最佳代码的一半,但仍然是使用字节的版本的两倍。