算法 | 输出长度 | 速度 | 安全性 | 主要用途 | 优点 | 缺点 | 使用建议 |
---|---|---|---|---|---|---|---|
MD5 | 128位(16字节) | 非常快 | 低 | 文件校验,快速比较 | 计算速度快,实现简单 | 已被破解,存在碰撞 | 仅用于非安全场景的校验 |
SHA1 | 160位(20字节) | 快 | 中低 | Git版本控制,文件校验 | 较MD5更安全,速度适中 | 已被证实可碰撞 | 建议迁移到SHA2 |
SHA256 | 256位(32字节) | 中等 | 高 | 数字签名,区块链 | 安全性高,使用广泛 | 计算较慢 | 推荐用于安全场景 |
SHA512 | 512位(64字节) | 中等 | 很高 | 高安全性要求场景 | 安全性极高 | 计算资源消耗大 | 用于关键安全场景 |
BCrypt | 184位(23字节) | 慢 | 很高 | 密码存储 | 自动加盐,防彩虹表 | 计算慢,资源消耗大 | 推荐用于密码哈希 |
PBKDF2 | 可变 | 可配置 | 高 | 密钥派生,密码存储 | 可配置迭代次数 | 计算资源消耗大 | 适合密码和密钥存储 |
HMAC | 取决于哈希算法 | 中等 | 高 | 消息认证,API安全 | 提供认证和完整性 | 需要密钥管理 | 推荐用于API认证 |
Argon2 | 可变 | 可配置 | 极高 | 密码哈希 | 内存难度可调,最新设计 | 实现复杂 | 推荐用于新项目密码存储 |
算法 | 包路径 | 输出长度 | 性能 | 使用场景 | 代码示例 | 特点 |
---|---|---|---|---|---|---|
MD5 | crypto/md5 | 16字节 | 很快 | 非加密校验 | md5.Sum(data) | 简单快速,不安全 |
SHA1 | crypto/sha1 | 20字节 | 快 | 数据校验 | sha1.Sum(data) | 较MD5安全,但不推荐 |
SHA256 | crypto/sha256 | 32字节 | 中等 | 加密场景 | sha256.Sum256(data) | 安全性高,使用广泛 |
SHA512 | crypto/sha512 | 64字节 | 中等 | 高安全场景 | sha512.Sum512(data) | 最高安全性 |
Adler32 | hash/adler32 | 4字节 | 极快 | 非加密校验 | adler32.Checksum(data) | 适合数据完整性快速校验 |
CRC32 | hash/crc32 | 4字节 | 极快 | 数据校验 | crc32.ChecksumIEEE(data) | 适合数据完整性校验 |
FNV32 | hash/fnv | 4字节 | 极快 | 哈希表 | fnv.New32().Sum(data) | 适合短字符串哈希 |
FNV64 | hash/fnv | 8字节 | 极快 | 哈希表 | fnv.New64().Sum(data) | FNV32的64位版本,其实还有128位版本,不单独列出了 |
BLAKE2b | golang.org/x/crypto/blake2b | 可变(最大64字节) | 快 | 加密场景 | blake2b.New256(nil) | 现代加密哈希函数,还有BLAKE2s变种 |
BCrypt | golang.org/x/crypto/bcrypt | 24字节 | 慢 | 密码存储 | bcrypt.GenerateFromPassword() | 专门用于密码哈希 |
var n int64 var testBytes []byte func BenchmarkHash(b *testing.B) { sizes := []int64{32, 64, 128, 256, 512, 1024} for _, n = range sizes { testBytes = make([]byte, n) readN, err := rand.Read(testBytes) if readN != int(n) { panic(fmt.Sprintf("expect %d but got %d", n, readN)) } if err != nil { panic(err) } b.Run(fmt.Sprintf("Sha1-%d", n), benchmarkSha1) b.Run(fmt.Sprintf("Sha256-%d", n), BenchmarkSha256) b.Run(fmt.Sprintf("Sha256SIMD-%d", n), BenchmarkSha256SIMD) b.Run(fmt.Sprintf("Sha512-%d", n), BenchmarkSha512) b.Run(fmt.Sprintf("MD5-%d", n), BenchmarkMD5) b.Run(fmt.Sprintf("Fnv-%d", n), BenchmarkFnv) b.Run(fmt.Sprintf("Adler32-%d", n), BenchmarkAdler32) b.Run(fmt.Sprintf("Crc32-%d", n), BenchmarkCrc32) b.Run(fmt.Sprintf("CityHash-%d", n), BenchmarkCityhash) b.Run(fmt.Sprintf("FarmHash-%d", n), BenchmarkFarmhash) b.Run(fmt.Sprintf("Farmhash_dgryski-%d", n), BenchmarkFarmhash_dgryski) b.Run(fmt.Sprintf("Murmur3-%d", n), BenchmarkMurmur3) b.Run(fmt.Sprintf("Highwayhash-%d", n), BenchmarkHighwayhash) b.Run(fmt.Sprintf("XXHash64-%d", n), BenchmarkXXHash64) b.Run(fmt.Sprintf("XXHash64_ASM-%d", n), BenchmarkXXHash64_ASM) b.Run(fmt.Sprintf("MapHash64-%d", n), BenchmarkMapHash64) b.Run(fmt.Sprintf("StdMapHash64-%d", n), BenchmarkStdMapHash64) b.Run(fmt.Sprintf("ChibiHash64-%d", n), BenchmarkChibiHash) b.Run(fmt.Sprintf("Blake2b-%d", n), BenchmarkBlake2b) fmt.Println() } } func benchmarkSha1(b *testing.B) { x := sha1.New() b.SetBytes(n) b.ResetTimer() for i := 0; i < b.N; i++ { x.Reset() x.Write(testBytes) _ = x.Sum(nil) } } ... // 其它hash算法这里写了一个通用的 hash benchmark 函数,用来测试不同 hash 算法在不同大小数据(32, 64, 128, 256, 512, 1024字节)下的性能。同时将 benchmark 结果输出到一个 csv 文件,便于生成图表。我找了一台 Linux/AMD64 的服务器,进行了测试,方便测试 CPU 的高级性能。
Intel(R) Xeon(R) Gold 6271C CPU @ 2.60GHz, 96 核 Linux 5.10.0-1.0.0.40 796GB 内存以 hash 1024 个字节为例(其它字节性能差不多),以下是各种 hash 算法的性能对比(包括吞吐):
BenchmarkHash/Sha1-1024-96 797005 1471 ns/op 695.96 MB/s BenchmarkHash/Sha256-1024-96 377248 3172 ns/op 322.82 MB/s BenchmarkHash/Sha256SIMD-1024-96 378414 3176 ns/op 322.46 MB/s BenchmarkHash/Sha512-1024-96 507495 2353 ns/op 435.16 MB/s BenchmarkHash/MD5-1024-96 725233 1648 ns/op 621.33 MB/s BenchmarkHash/Fnv-1024-96 911083 1312 ns/op 780.48 MB/s BenchmarkHash/Adler32-1024-96 2734160 434.1 ns/op 2359.04 MB/s BenchmarkHash/Crc32-1024-96 15229868 78.35 ns/op 13069.26 MB/s BenchmarkHash/CityHash-1024-96 1774196 676.4 ns/op 1513.94 MB/s BenchmarkHash/FarmHash-1024-96 2465469 487.2 ns/op 2101.90 MB/s BenchmarkHash/Farmhash_dgryski-1024-96 10912843 109.9 ns/op 9316.16 MB/s BenchmarkHash/Murmur3-1024-96 6018717 199.2 ns/op 5141.23 MB/s BenchmarkHash/Highwayhash-1024-96 9591049 125.0 ns/op 8188.84 MB/s BenchmarkHash/XXHash64-1024-96 11106692 108.0 ns/op 9477.11 MB/s BenchmarkHash/XXHash64_ASM-1024-96 12232465 98.09 ns/op 10439.81 MB/s BenchmarkHash/MapHash64-1024-96 25519408 47.02 ns/op 21777.23 MB/s BenchmarkHash/StdMapHash64-1024-96 9233738 130.2 ns/op 7864.52 MB/s BenchmarkHash/ChibiHash64-1024-96 2550945 470.3 ns/op 2177.50 MB/s BenchmarkHash/Blake2b-1024-96 952615 1258 ns/op 814.09 MB/s
这里面MapHash明显是最优秀的一种哈希算法:47.02 ns/op, 每秒可以处理21777.23 MB的数据。MapHash算法就是 Go 运行时中为 Map 的哈希所实现的 hash 算法。即使标准库暴露了这个算法,但是还是没有直接通过 hack 的方式使用它性能更高。
//go:noescape //go:linkname memhash runtime.memhash func memhash(p unsafe.Pointer, h, s uintptr) uintptr // 堆代码 duidaima.com type stringStruct struct { str unsafe.Pointer len int } func MemHash(data []byte) uint64 { ss := (*stringStruct)(unsafe.Pointer(&data)) return uint64(memhash(ss.str, 0, uintptr(ss.len))) }这里我们不考虑安全和碰撞,总是把种子设置为 0。你也看到了,这个算法实际是在runtime.memhash中实现的。找到它:
// memhash should be an internal detail, // but widely used packages access it using linkname. // Notable members of the hall of shame include: // - github.com/aacfactory/fns // - github.com/dgraph-io/ristretto // - github.com/minio/simdjson-go // - github.com/nbd-wtf/go-nostr // - github.com/outcaste-io/ristretto // - github.com/puzpuzpuz/xsync/v2 // - github.com/puzpuzpuz/xsync/v3 // - github.com/authzed/spicedb // - github.com/pingcap/badger // // Do not remove or change the type signature. // See go.dev/issue/67401. // //go:linkname memhash func memhash(p unsafe.Pointer, h, s uintptr) uintptr func memhash32(p unsafe.Pointer, h uintptr) uintptr func memhash64(p unsafe.Pointer, h uintptr) uintptr文件不太好找,在src/runtime/alg.go文件中。注意有趣的注释,被点名的使用了这个函数的库。Go 团队正在推进拒绝 hack 使用这些内部函数的go:linkname方式,但是当前 Go 生态圈的库有很多都这么使用,不好搞。很明显,这个实现是通过 C 或者 ASM 实现的。找到它。具体都在src/runtime/asm_xxx.s文件中,比如src/runtime/asm_amd64.s文件中有memhash的实现:
TEXT runtime·memhash<ABIInternal>(SB),NOSPLIT,$0-32 // AX = ptr to data // BX = seed // CX = size CMPB runtime·useAeshash(SB), $0 JEQ noaes JMP aeshashbody<>(SB) noaes: JMP runtime·memhashFallback<ABIInternal>(SB)可以看到,如果 CPU 支持 Aes 指令,它会使用 Aes 指令来实现 hash 算法,否则会使用runtime·memhashFallback纯内存算法的函数实现。也正是使用了 Aes 指令,所以 maphash 的性能非常高,无愧于性能之王。
// AX: data // BX: hash seed // CX: length // At return: AX = return value TEXT aeshashbody<>(SB),NOSPLIT,$0-0 ..具体我们就不解读了,不可读,主要判断要 hash 的数据的长度,走到不同的分支中,使用种子和数据进行异或操作(PXOR)、使用AESENC指令执行多轮 AES 加密。你可以通过cat /proc/cpuinfo | grep -m 1 -i aes查看你的 CPU 是否支持 aes 指令: