为什么 strings.HasPrefix 比 bytes.HasPrefix 更快?

10

在我的代码中,我有这样的基准测试:

const STR = "abcd"
const PREFIX = "ab"
var STR_B = []byte(STR)
var PREFIX_B = []byte(PREFIX)

func BenchmarkStrHasPrefix(b *testing.B) {
    for i := 0; i < b.N; i++ {
        strings.HasPrefix(STR, PREFIX)
    }
}

func BenchmarkBytHasPrefix(b *testing.B) {
    for i := 0; i < b.N; i++ {
        bytes.HasPrefix(STR_B, PREFIX_B)
    }
}

我有点困惑于结果:

BenchmarkStrHasPrefix-4    300000000    4.67 ns/op
BenchmarkBytHasPrefix-4    200000000    8.05 ns/op

为什么会有高达2倍的差异?

谢谢。


1
STRSTR_BPREFIXPREFIX_B的值是什么? - Ismail Badawi
@IsmailBadawi 更新了示例) - Sergey Kamardin
请注意,在我的Core2Duo P8600上,这两个基准测试的表现相同(约为20 ns/op),而在i5-2400S上,它们与您的类似-5.5 vs 8.5 ns/op。 - tomasz
1个回答

17
主要原因在于调用 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

在某些平台上,这可能会被内联,从而不会产生额外的调用成本。


1
@0x434D53 +1,例如,在asm_amd64.s(第1312行(runtime·eqstring)和第1652行(bytes·Equal))中,这段代码有什么区别? - Sergey Kamardin
@icza 谢谢,但您能更明确地解释一下吗?=) - Sergey Kamardin
1
@0x434D53 进一步调查后,见编辑后的答案。 - icza
3
在运行go run时添加-gcflags -m可以告诉你哪些函数被内联了,结果只有strings.HasPrefix,而没有bytes.HasPrefix。如果禁用内联(-gcflags -l),你会得到类似的结果(bytes慢了约10%)。然而,如果禁用所有优化(-gcflags -N),比较bytes的基准测试结果会更好。至少在我的i5上如此。话虽如此,这种差异似乎取决于编译的黑魔法。 - tomasz
1
@0x434D53 我也没有深入研究过,但我猜测比较字符串常量时,它会在编译时处理,而比较非常量字符串时,长度比较会被内联。更多细节请参见这个问题 - icza
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接