Skip to content

Commit

Permalink
3-10~13 校对格式
Browse files Browse the repository at this point in the history
  • Loading branch information
litianshi committed Nov 22, 2022
1 parent 0ba9683 commit fdecca5
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 37 deletions.
40 changes: 21 additions & 19 deletions chapter/3-Data-types/3-10-slice-and-memory-leaks.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
## 3.10 切片与内存泄漏

本节将展示对现有切片或数组进行切片在某些情况下会导致内存泄漏。 我们将讨论两种情况:一种是容量泄漏的情况,另一种是与指针有关的情况。
本节将展示对现有切片或数组进行切片在某些情况下会导致内存泄漏。我们将讨论两种情况:一种是容量泄漏的情况,另一种是与指针有关的情况。

### 3.10.1 容量泄漏

对于第一种情况,让我们想象实现一个自定义二进制协议。 一条消息可以包含一百万字节,前五个字节代表消息类型。 在我们的代码中,我们使用这些消息,出于审计目的,我们希望将最新的 1000 条消息类型存储在内存中。 我们的功能框架如下:
对于第一种情况,让我们想象实现一个自定义二进制协议。一条消息可以包含一百万字节,前五个字节代表消息类型。在我们的代码中,我们使用这些消息,出于审计目的,我们希望将最新的 1000 条消息类型存储在内存中。我们的功能框架如下:

```go
func consumeMessages() {
Expand All @@ -20,15 +20,15 @@ func getMessageType(msg []byte) []byte {
}
```

`getMessageType` 函数通过对输入切片进行切片来计算消息类型。 我们测试了这个实现,一切都很好。 但是,当我们部署我们的应用程序时,我们注意到我们的应用程序消耗了大约 1 GB 的内存。 这怎么可能?
`getMessageType` 函数通过对输入切片进行切片来计算消息类型。我们测试了这个实现,一切都很好。但是,当我们部署我们的应用程序时,我们注意到我们的应用程序消耗了大约 1 GB 的内存。这怎么可能?

使用 `msg[:5]``msg` 进行切片操作会创建一个 5 长度的切片。 但是,它的容量与初始切片的容量相同。 剩余的元素仍然分配在内存中,即使最终 `msg` 将不再被引用。 让我们看一个具有一百万字节的大消息长度的示例:
使用 `msg[:5]``msg` 进行切片操作会创建一个 5 长度的切片。但是,它的容量与初始切片的容量相同。剩余的元素仍然分配在内存中,即使最终 `msg` 将不再被引用。让我们看一个具有一百万字节的大消息长度的示例:

![](https://img.exciting.net.cn/19.png)

正如我们在这个图中可以注意到的,切片的后备数组在切片操作之后仍然包含一百万字节。 因此,如果我们在内存中保留 1000 条消息,而不是存储大约 5 KB,我们将保留大约 1 GB。
正如我们在这个图中可以注意到的,切片的后备数组在切片操作之后仍然包含一百万字节。因此,如果我们在内存中保留 1000 条消息,而不是存储大约 5 KB,我们将保留大约 1 GB。

那么我们能做些什么来解决这个问题呢? 通过制作切片副本而不是原始切片 `msg`
那么我们能做些什么来解决这个问题呢?通过制作切片副本而不是原始切片 `msg`

```go
func getMessageType(msg []byte) []byte {
Expand All @@ -38,9 +38,10 @@ func getMessageType(msg []byte) []byte {
}
```

当我们执行复制时,`msgType` 是一个长度为 5、容量为 5 的切片,而与接收到的消息的大小无关。 因此,我们将只存储每种消息类型的 5 个字节。
当我们执行复制时,`msgType` 是一个长度为 5、容量为 5 的切片,而与接收到的消息的大小无关。因此,我们将只存储每种消息类型的 5 个字节。

> **Note** 在这里,`getMessageType` 将返回初始切片的缩小版本:一个长度为 5、容量为 5 的切片。 然而,GC 是否能够从字节 5 中回收不可访问的空间? Go 规范没有正式指定行为。
> **Note** 在这里,`getMessageType` 将返回初始切片的缩小版本:一个长度为 5、容量为 5 的切片。然而,GC 是否能够从字节 5 中回收不可访问的空间?Go 规范没有正式指定行为。
>
> 但是,通过使用 `runtime.Memstats`,我们可以记录有关内存分配器的统计信息,例如在堆上分配的字节数:
```go
func printAlloc() {
Expand All @@ -49,8 +50,9 @@ func printAlloc() {
fmt.Printf("%d KB\n", m.Alloc/1024)
}
```
>如果我们在调用 `getMessageType``runtime.GC()` 之后调用此函数来强制运行垃圾回收,我们会注意到无法回收的空间没有被回收。 整个后备阵列仍将存在于内存中。
因此,使用完整的切片表达式不是一个有效的选项(除非 Go 的未来更新会解决它)。
>如果我们在调用 `getMessageType``runtime.GC()` 之后调用此函数来强制运行垃圾回收,我们会注意到无法回收的空间没有被回收。整个后备阵列仍将存在于内存中。
>
>因此,使用完整的切片表达式不是一个有效的选项(除非 Go 的未来更新会解决它)。
使用完整的切片表达式来解决这个问题怎么样?

```go
Expand All @@ -59,11 +61,11 @@ func getMessageType(msg []byte) []byte {
}
```

根据经验,我们必须记住,对大切片或数组进行切片可能会导致潜在的高内存消耗。 实际上,GC 不会回收剩余空间,尽管只使用了几个元素,我们仍可以保留一个大的后备数组。 使用切片复制是防止这种情况的解决方案。
根据经验,我们必须记住,对大切片或数组进行切片可能会导致潜在的高内存消耗。实际上,GC 不会回收剩余空间,尽管只使用了几个元素,我们仍可以保留一个大的后备数组。使用切片复制是防止这种情况的解决方案。

###3.10.2 切片与指针
### 3.10.2 切片与指针

我们已经看到,由于切片容量,切片会导致泄漏。 然而,元素呢? 这些仍然是支持数组的一部分,但超出了长度范围。 GC 会收集它们吗?
我们已经看到,由于切片容量,切片会导致泄漏。然而,元素呢?这些仍然是支持数组的一部分,但超出了长度范围。GC 会收集它们吗?

让我们使用包含字节切片的 `Foo` 结构来研究这个问题:

Expand All @@ -79,7 +81,7 @@ type Foo struct {
* 迭代每个 `Foo` 元素,并为每个 `v` 切片分配1 MB
* 使用切片调用仅返回前两个元素的 `keepFirstTwoElementsOnly`,然后调用GC

我们想看看调用 `keepFirstTwoElementsOnly` 和 GC 后内存的行为。 这是 Go 中的场景(我们将重用之前定义的 `printAlloc` 函数):
我们想看看调用 `keepFirstTwoElementsOnly` 和 GC 后内存的行为。这是 Go 中的场景(我们将重用之前定义的 `printAlloc` 函数):

```go
func main() {
Expand All @@ -106,17 +108,17 @@ func keepFirstTwoElementsOnly(foos []Foo) []Foo {

我们分配 `foos` 切片,为每个元素分配1 MB的切片,然后调用 `keepFirstTwoElementsOnly` 和GC。最后,我们使用 `runtime.KeepAlive` 在GC之后保留对两个变量的引用,这样就不会被收集。

我们可能期望 GC 收集剩余的 998 个 `Foo` 元素和为切片分配的数据,因为这些元素无法再访问。 然而,事实并非如此。 例如,代码可以打印以下内容:
我们可能期望 GC 收集剩余的 998 个 `Foo` 元素和为切片分配的数据,因为这些元素无法再访问。然而,事实并非如此。例如,代码可以打印以下内容:

```go
83 KB
1024072 KB
1024073 KB
```

第一步分配大约 83 KB 的数据。 事实上,我们分配了 1000 个 Foo 的零值。 第二步为每个切片分配 1 MB,这增加了内存。 但是,我们可以注意到 GC 在最后一步之后没有收集剩余的 998 个元素。 什么原因?
第一步分配大约 83 KB 的数据。事实上,我们分配了 1000 个 Foo 的零值。第二步为每个切片分配 1 MB,这增加了内存。但是,我们可以注意到 GC 在最后一步之后没有收集剩余的 998 个元素。什么原因?

规则如下,在使用 slice 时必须牢记:如果元素是指针或带有指针字段的结构,则 GC 不会回收这些元素。 在这里,由于 `Foo` 包含一个切片(并且切片是支持数组顶部的指针),剩余的 998 个 `Foo` 元素及其切片将不会被回收。 因此,即使这 998 个元素不能再被访问,只要 `keepFirstTwoElementsOnly` 返回的变量被引用,它们就会留在内存中。
规则如下,在使用 slice 时必须牢记:如果元素是指针或带有指针字段的结构,则 GC 不会回收这些元素。在这里,由于 `Foo` 包含一个切片(并且切片是支持数组顶部的指针),剩余的 998 个 `Foo` 元素及其切片将不会被回收。因此,即使这 998 个元素不能再被访问,只要 `keepFirstTwoElementsOnly` 返回的变量被引用,它们就会留在内存中。

那么有哪些选项可以确保我们不会泄露剩余的 `Foo` 元素呢?

Expand All @@ -143,9 +145,9 @@ func keepFirstTwoElementsOnly(foos []Foo) []Foo {
}
```

在这里,我们返回一个长度为 2、容量为 1000 的切片,但我们将剩余元素的切片设置为 nil。 因此,GC 将能够收集 998 个支持数组。
在这里,我们返回一个长度为 2、容量为 1000 的切片,但我们将剩余元素的切片设置为 nil。因此,GC 将能够收集 998 个支持数组。

那么,哪个选项是最好的呢? 如果我们不想保留 1000 个元素的容量,第一个选项可能是最好的。 然而,该决定也可以取决于元素的比例。 让我们看一个可视化示例,假设切片包含 n 个元素,我们希望在其中保留 i 个元素:
那么,哪个选项是最好的呢?如果我们不想保留 1000 个元素的容量,第一个选项可能是最好的。然而,该决定也可以取决于元素的比例。让我们看一个可视化示例,假设切片包含 n 个元素,我们希望在其中保留 i 个元素:

![](https://img.exciting.net.cn/20.png)

Expand Down
14 changes: 7 additions & 7 deletions chapter/3-Data-types/3-11-inefficient-map-initialization.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
## 3.11 低效的map初始化
## 3.11 低效的 map 初始化

本节将讨论我们已经在切片初始化中看到的类似问题,但这次使用的是 map 。但首先,我们需要了解有关如何在 Go 中实现 map 的基础知识,以了解为什么调整如何初始化 map 很重要。

### 3.11.1 概念

map 提供了键/值对的无序集合,其中所有键都是不同的。
map 提供了键/值对的无序集合,其中所有键都是不同的。

在 Go 中,map 是基于 hashtable 数据结构的。在内部,哈希表是一个桶数组,每个桶是一个指向键/值对数组的指针,如下图所示:

Expand Down Expand Up @@ -40,12 +40,12 @@ m := map[string]int{

在内部, map 将由一个由单个条目组成的数组支持;因此,一个桶。现在,如果我们添加一百万个元素会发生什么?在这种情况下,单个条目是不够的,因为在最坏的情况下,找到一个密钥意味着要遍历数千个存储桶。这就是为什么 map 应该能够自动增长以应对元素数量的原因。

当 map 增长时,它的桶数将增加一倍。 map 成长的条件是什么?
当 map 增长时,它的桶数将增加一倍。map 成长的条件是什么?

* 如果桶中的平均项目数(称为负载因子)大于一个常数值。这个常数等于6.5(但在未来的版本中可能会改变,因为它是Go内部的)。
* 如果桶中的平均项目数(称为负载因子)大于一个常数值。这个常数等于6.5(但在未来的版本中可能会改变,因为它是 Go 内部的)。
* 如果溢出的桶太多(包含超过八个元素)。

当map增长时,所有的键将再次分派到所有的桶。这就是为什么在最坏的情况下,插入一个键可能是一个 _O(n)_ 操作,其中n是 map 中元素的总数。
当 map 增长时,所有的键将再次分派到所有的桶。这就是为什么在最坏的情况下,插入一个键可能是一个 _O(n)_ 操作,其中n是 map 中元素的总数。

我们已经看到使用切片,如果我们预先知道要添加到切片中的元素数量,我们可以用给定的大小或容量对其进行初始化。它避免了必须不断重复昂贵的切片增长操作。这个想法类似于 map 。实际上,我们可以使用 `make` 内置函数在创建 map 时提供初始大小。例如,如果我们要初始化一个包含一百万个元素的 map ,可以这样完成:

Expand All @@ -57,13 +57,13 @@ m := make(map[string]int, 1_000_000)

通过指定大小,我们可以提示预期进入 map 的元素数量。在内部,将使用适当数量的桶创建 map 以存储一百万个元素。因此,它将节省大量计算时间,因为 map 不必动态创建存储桶并处理存储桶重新平衡。

此外,指定大小n并不意味着制作具有最多n个元素的 map :如果需要,我们 仍然可以添加超过n个元素。相反,这意味着要求Go运行时为至少n个元素分配一个空间,如果我们预先知道 大小,这将很有帮助。
此外,指定大小n并不意味着制作具有最多n个元素的 map :如果需要,我们仍然可以添加超过n个元素。相反,这意味着要求 Go 运行时为至少n个元素分配一个空间,如果我们预先知道大小,这将很有帮助。

为了理解为什么指定大小很重要,让我们运行两个基准测试。前者将在不设置初始大小的 map 中插入一百万个元素,而后者在使用大小初始化的 map 中。

```go
BenchmarkMapWithoutSize-4 6 227413490 ns/op
BenchmarkMapWithSize-4 13 91174193 ns/op
BenchmarkMapWithSize-4 13 91174193 ns/op
```

具有初始大小的第二个版本大约快60%。实际上,通过提供大小,我们可以防止 map 不断增长以应对插入的元素。
Expand Down
16 changes: 8 additions & 8 deletions chapter/3-Data-types/3-12-map-and-memory-leaks.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
m := make(map[int][128]byte)
```

m的每个值都是⼀个 128 字节的数组。
m 的每个值都是⼀个 128 字节的数组。

我们将执⾏以下场景:

Expand All @@ -25,20 +25,20 @@ m := make(map[int][128]byte)
printAlloc()
// Add elements
for i := 0; i < n; i++ {
m[i] = randBytes()
m[i] = randBytes()
}
printAlloc()
// Remove elements
for i := 0; i < n; i++ {
delete(m, i)
delete(m, i)
}
// End
runtime.GC()
printAlloc()
runtime.KeepAlive(m)
```

⾸先,我们分配⼀个空映射,添加⼀百万个元素,删除⼀百万个元素,然后运⾏⼀次GC。我们还确保使⽤ `runtime.KeepAlive` 保留对 map 的引⽤,这样 map 也不会被收集。
⾸先,我们分配⼀个空映射,添加⼀百万个元素,删除⼀百万个元素,然后运⾏⼀次 GC。我们还确保使⽤ `runtime.KeepAlive` 保留对 map 的引⽤,这样 map 也不会被收集。

```go
0 MB
Expand All @@ -58,15 +58,15 @@ type hmap struct {
}
```

添加⼀百万个元素后, B的值等于 18,即 2^18 = 262,144 个桶。那么,当我们删除⼀百万个元素时, B的值是多少?仍然是 18。因此, map 仍然包含相同数量的桶。
添加⼀百万个元素后,B的值等于 18,即 2^18 = 262,144 个桶。那么,当我们删除⼀百万个元素时,B的值是多少?仍然是 18。因此,map 仍然包含相同数量的桶。

原因是map中的桶数不能缩⼩。因此,从 map 中删除元素不会影响现有存储桶的数量;它只是将存储桶中的插槽归零。⼀张 map 只能增⻓并拥有更多的桶;它永远不会缩⼩。
原因是 map 中的桶数不能缩⼩。因此,从 map 中删除元素不会影响现有存储桶的数量;它只是将存储桶中的插槽归零。⼀张 map 只能增⻓并拥有更多的桶;它永远不会缩⼩。

在前⾯的⽰例中,我们从 461 MB 变为 293 MB,因为元素已被收集,但运⾏ GC 并不会影响 map 本⾝。甚⾄额外存储桶的数量(由于溢出⽽创建的存储桶)也保持不变。

让我们退后⼀步讨论⼀下 map ⽆法缩⼩的事实何时会成为问题。

想象⼀下使⽤ `map[int][128]byte` 构建缓存。此映射为每个客⼾ ID(int) 保存⼀个 128 字节的序列。现在,假设我们要保存最后 1000 个客⼾。 map ⼤⼩将保持不变,因此我们不必担⼼ map ⽆法缩⼩。
想象⼀下使⽤ `map[int][128]byte` 构建缓存。此映射为每个客⼾ ID(int) 保存⼀个 128 字节的序列。现在,假设我们要保存最后 1000 个客⼾。map ⼤⼩将保持不变,因此我们不必担⼼ map ⽆法缩⼩。

但是,现在假设我们要存储⼀⼩时的数据。同时,我们公司决定为⿊⾊星期五做⼀个⼤促销,⼀⼩时内,我们可以让数百万客⼾连接到我们的系统。在这种情况下,即使在⼏天之后,⼀旦⿊⾊星期五结束,我们的 map 将包含与⾼峰时段相同数量的存储桶。因此,它解释了为什么我们会遇到在这种情况下不会显着减少的⾼内存消耗。

Expand All @@ -82,7 +82,7 @@ type hmap struct {
| 添加一百万个元素 | 461MB | 182MB |
| 删除所有元素并运行GC | 293MB | 38MB |

我们可以注意到,在删除所有元素之后,所需的内存量对于 `map[int]*[128]byte` 类型的重要性明显降低。 此外,在这种情况下,在高峰时间,由于一些优化以减少消耗的内存,所需的内存量不太重要。
我们可以注意到,在删除所有元素之后,所需的内存量对于 `map[int]*[128]byte` 类型的重要性明显降低。此外,在这种情况下,在高峰时间,由于一些优化以减少消耗的内存,所需的内存量不太重要。

> **Note** 我们还应该注意,如果键或值超过128字节,Go不会将其直接存储在map桶中。
相反,Go将存储一个引用键或值的指针。
Expand Down
Loading

0 comments on commit fdecca5

Please sign in to comment.