今天在看切片内存分配的源码,makeslice 函数在内存分配前先使用 MaxUintptr 函数来判断内存分配是否越界:

func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
// 先判断是否越界
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}

// 内存分配
return mallocgc(mem, et, true)
}

出于好奇看了一下 MaxUintptr 的源码:

// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package math

import "runtime/internal/sys"

const MaxUintptr = ^uintptr(0)

// MulUintptr returns a * b and whether the multiplication overflowed.
// On supported platforms this is an intrinsic lowered by the compiler.
func MulUintptr(a, b uintptr) (uintptr, bool) {
if a|b < 1<<(4*sys.PtrSize) || a == 0 {
return a * b, false
}
overflow := b > MaxUintptr/a
return a * b, overflow
}

由源码可知,MulUintptr 接收两个参数,分别是要分配的类型大小 a 和要分配的数量 b,计算后返回要分配的内存空间以及是否溢出。

位运算表达式的含义

a|b < 1<<(4*sys.PtrSize) 这个位运算表达式看起来非常复杂,我们来剖析一下。

sys.PtrSize 表示系统指针大小,在 32 位机器中,sys.PtrSize = 4,64 位机器中,sys.PtrSize = 8<< 是左移运算符,我们知道在运算中左移 1 位就是一次乘 2 操作,因此 1<<(4*sys.PtrSize) 表示的其实就是 2^(4*sys.PtrSize)

综上,我们可以把表达式变形一下:

  • 在 32 位机器中,4*sys.PtrSize = 4 * 4 = 16 表达式可以写作 a|b < 2^16,可证明 ab 均小于 2^16,a * b 必然小于 2^32
  • 在 64 位机器中,4*sys.PtrSize = 4 * 8 = 32 表达式可以写作 a|b < 2^32,可证明 ab 均小于 2^32,a * b 必然小于 2^64

那么 2^32 与 2^64 又代表着什么呢?

何为溢出?

我们常说的 64 位系统或 32 位系统,其中的「位数」决定了计算机的寻址空间,即 CPU 对于内存的寻址能力。通俗地讲,就是 CPU 最多能够使用的内存。32 位系统的寻址空间为 2^32,64 位系统的寻址空间为 2^64。

因此,a|b < 1<<(4*sys.PtrSize) 的含义是:要分配的内存是否小于寻址空间。若小于寻址空间,即不存在溢出,此时函数返回 overflow = false

^uintptr(0) 是什么?

unintptr 是 Go 中的自定义整型:

#ifdef _64BIT
typedef uint64 uintptr;
#else
typedef uint32 uintptr;
#endif
  • 32 位系统中,unitptr 代表 uint32,占 4 字节,^uintptr(0) 等于 ^uint32(0),即 2^32 - 1
  • 64 位系统中,unitptr 代表 uint64,占 8 字节,^uintptr(0) 等于 ^uint64(0),即 2^64 - 1

因此,overflow := b > MaxUintptr/a 可以变形为:

  • 在 32 位机器中:overflow := b > (2^32 - 1)/a
  • 在 64 位机器中:overflow := b > (2^64 - 1)/a

这样就很好理解啦。

代码逻辑思考

如果由我来写这段代码,我无法想到这样的写法,大概率会使用 a * b < MaxUintptr 来暴力解决问题。然而计算机中乘法与除法并不意味着更快的计算过程,他们的本质还是使用累加器,而位运算才意味着高效。因此,先使用 a|b < 1<<(4*sys.PtrSize) 作为判断是非常巧妙的做法。

存在的疑问

if a|b < 1<<(4*sys.PtrSize) || a == 0 {
return a * b, false
}

我对以上这句逻辑判断存在疑问,根据短路求值,把 a == 0 写在前面是否更好呢?以及是否需要把 b == 0 也补上?准备提个 issue 问问开发者吧。

参考资料