主要原因在于调用
bytes.HasPrefix()
和
strings.HasPrefix()
的成本不同。正如@tomasz在他的评论中指出的那样,
strings.HashPrefix()
默认情况下是内联的,而
bytes.HasPrefix()
则不是。
进一步的原因在于参数类型不同:
bytes.HasPrefix()
使用2个slice(2个slice描述符)。
strings.HasPrefix()
使用2个字符串(2个字符串头)。切片描述符包含一个指针和2个整数:长度和容量,请参见
reflect.SliceHeader
。字符串头仅包含指针和一个整数:长度,请参见
reflect.StringHeader
。
如果我们手动将
HasPrefix()
函数展开到基准测试函数中,则可以证明这一点,从而消除调用成本(零成本)。通过将它们内联,将不会对它们进行任何函数调用。
HasPrefix()
实现:
// HasPrefix tests whether the byte slice s begins with prefix.
func HasPrefix(s, prefix []byte) bool {
return len(s) >= len(prefix) && Equal(s[0:len(prefix)], prefix)
}
// HasPrefix tests whether the string s begins with prefix.
func HasPrefix(s, prefix string) bool {
return len(s) >= len(prefix) && s[0:len(prefix)] == prefix
}
内联后的基准函数:
func BenchmarkStrHasPrefix(b *testing.B) {
s, prefix := STR, PREFIX
for i := 0; i < b.N; i++ {
_ = len(s) >= len(prefix) && s[0:len(prefix)] == prefix
}
}
func BenchmarkBytHasPrefix(b *testing.B) {
s, prefix := STR_B, PREFIX_B
for i := 0; i < b.N; i++ {
_ = len(s) >= len(prefix) && bytes.Equal(s[0:len(prefix)], prefix)
}
}
运行这些代码,你会得到非常接近的结果:
BenchmarkStrHasPrefix-2 300000000 5.88 ns/op
BenchmarkBytHasPrefix-2 200000000 6.17 ns/op
可能导致内联基准测试结果略有差异的原因是两个函数都通过切片
string
和
[]byte
操作检测前缀的存在。由于
string
可以比较而字节切片则不行,所以相对于
BenchmarkStrHasPrefix()
函数,
BenchmarkBytHasPrefix()
需要额外调用一个函数
bytes.Equal()
(此外,这个额外的函数调用还包括复制其参数:2个切片头部)。
其他可能导致原始结果略有差异的因素:BenchMarkStrHasPrefix()使用的参数是常量,而BenchMarkBytHasPrefix()使用的参数是变量。
不必过于担心性能差异,两个函数都在几个纳秒内完成。
请注意,
bytes.Equal()
的“实现”:
func Equal(a, b []byte) bool // ../runtime/asm_$GOARCH.s
在某些平台上,这可能会被内联,从而不会产生额外的调用成本。
STR
、STR_B
、PREFIX
、PREFIX_B
的值是什么? - Ismail Badawi