7

Go 源码阅读:内存分配前的溢出判断

 3 years ago
source link: http://jalan.space/2021/07/11/2021/go-source-code-muluiniptr/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Go 源码阅读:内存分配前的溢出判断

0 Comments

今天在看切片内存分配的源码,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 问问开发者吧。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK