ホームページ > バックエンド開発 > Golang > Go で大きな配列を処理する: for 範囲または for ループを使用しますか?

Go で大きな配列を処理する: for 範囲または for ループを使用しますか?

リリース: 2023-07-24 14:47:53
転載
1366 人が閲覧しました
#Go の構文は比較的簡潔であることがわかっています。 <span style="font-size: 15px;">while</span><span style="font-size: 15px;">do...while</span># に対する C のようなサポートは提供されません。 # # およびその他のループ制御構文は使用できますが、1 つのステートメント (for ループ) のみが保持されます。

1

2

3

for i := 0; i < n; i++ {

    ... ...

}

ログイン後にコピー

ただし、古典的な 3 段階のループ ステートメントでは、反復オブジェクトの長さ n を取得する必要があります。これを考慮して、Go 開発者が配列、スライス、チャネル、マップなどの複合データ型を反復処理しやすくするために、Go では for ループのバリアント、つまり を提供しています。 for range<span style="font-size: 15px;"></span> ループ。

コピーコピーの問題

範囲は利便性をもたらしますが、Go 初心者にとってはいくつかの問題ももたらします。なぜなら、ユーザーは 1 つのことを理解する必要があるからです。つまり、for 範囲では、オブジェクトのコピーのみがループ式に参加するということです。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

func main() {

    var a = [5]int{1, 2, 3, 4, 5}

    var r [5]int

 

    fmt.Println("original a =", a)

 

    for i, v := range a {

        if i == 0 {

            a[1] = 12

            a[2] = 13

        }

        r[i] = v

    }

 

    fmt.Println("after for range loop, r =", r)

    fmt.Println("after for range loop, a =", a)

}

ログイン後にコピー

このコードは次の結果を出力すると思いますか?

1

2

3

original a = [1 2 3 4 5]

after for range loop, r = [1 12 13 4 5]

after for range loop, a = [1 12 13 4 5]

ログイン後にコピー

但是,实际输出是

1

2

3

original a = [1 2 3 4 5]

after for range loop, r = [1 2 3 4 5]

after for range loop, a = [1 12 13 4 5]

ログイン後にコピー

为什么会这样?原因是参与 for range 循环是 range 表达式的副本。也就是说,在上面的例子中,实际上参与循环的是 a 的副本,而不是真正的 a。

为了让大家更容易理解,我们把上面例子中的 for range 循环改写成等效的伪代码形式。

1

2

3

4

5

6

7

for i, v := range ac { //ac is a value copy of a

    if i == 0 {

        a[1] = 12

        a[2] = 13

    }

    r[i] = v

}

ログイン後にコピー

ac 是 Go 临时分配的连续字节序列,与 a 根本不是同一块内存空间。因此,无论 a 如何修改,它参与循环的副本 ac 仍然保持原始值,因此从 ac 中取出的 v 也依然是 a 的原始值,而不是修改后的值。

那么,问题来了,既然 for range 使用的是副本数据,那 for range 会比经典的 for 循环消耗更多的资源并且性能更差吗?

性能对比

基于副本复制问题,我们先使用基准示例来验证一下:对于大型数组,for range 是否一定比经典的 for 循环运行得慢?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

package main

 

import "testing"

 

func BenchmarkClassicForLoopIntArray(b *testing.B) {

 b.ReportAllocs()

 var arr [100000]int

 for i := 0; i < b.N; i++ {

  for j := 0; j < len(arr); j++ {

   arr[j] = j

  }

 }

}

 

func BenchmarkForRangeIntArray(b *testing.B) {

 b.ReportAllocs()

 var arr [100000]int

 for i := 0; i < b.N; i++ {

  for j, v := range arr {

   arr[j] = j

   _ = v

  }

 }

}

ログイン後にコピー

在这个例子中,我们使用 for 循环和 for range 分别遍历一个包含 10 万个 int 类型元素的数组。让我们看看基准测试的结果

1

2

3

4

5

6

7

8

$ go test -bench . forRange1_test.go

goos: darwin

goarch: amd64

cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz

BenchmarkClassicForLoopIntArray-8          47404             25486 ns/op               0 B/op          0 allocs/op

BenchmarkForRangeIntArray-8                37142             31691 ns/op               0 B/op          0 allocs/op

PASS

ok      command-line-arguments  2.978s

ログイン後にコピー

从输出结果可以看出,for range 的确会稍劣于 for 循环,当然这其中包含了编译器级别优化的结果(通常是静态单赋值,或者 SSA 链接)。

让我们关闭优化开关,再次运行压力测试。

1

2

3

4

5

6

7

8

9

$ go test -c -gcflags &#39;-N -l&#39; . -o forRange1.test

 $ ./forRange1.test -test.bench .

 goos: darwin

goarch: amd64

pkg: workspace/example/forRange

cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz

BenchmarkClassicForLoopIntArray-8           6734            175319 ns/op               0 B/op          0 allocs/op

BenchmarkForRangeIntArray-8                 5178            242977 ns/op               0 B/op          0 allocs/op

PASS

ログイン後にコピー

当没有编译器优化时,两种循环的性能都明显下降, for range 下降得更为明显,性能也更加比经典 for 循环差。

遍历结构体数组

上述性能测试中,我们的遍历对象类型是 int 值的数组,如果我们将 int 元素改为结构体会怎么样?for 和 for range 循环各自表现又会如何?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

package main

 

import "testing"

 

type U5 struct {

 a, b, c, d, e int

}

type U4 struct {

 a, b, c, d int

}

type U3 struct {

 b, c, d int

}

type U2 struct {

 c, d int

}

type U1 struct {

 d int

}

 

func BenchmarkClassicForLoopLargeStructArrayU5(b *testing.B) {

 b.ReportAllocs()

 var arr [100000]U5

 for i := 0; i < b.N; i++ {

  for j := 0; j < len(arr)-1; j++ {

   arr[j].d = j

  }

 }

}

func BenchmarkClassicForLoopLargeStructArrayU4(b *testing.B) {

 b.ReportAllocs()

 var arr [100000]U4

 for i := 0; i < b.N; i++ {

  for j := 0; j < len(arr)-1; j++ {

   arr[j].d = j

  }

 }

}

func BenchmarkClassicForLoopLargeStructArrayU3(b *testing.B) {

 b.ReportAllocs()

 var arr [100000]U3

 for i := 0; i < b.N; i++ {

  for j := 0; j < len(arr)-1; j++ {

   arr[j].d = j

  }

 }

}

func BenchmarkClassicForLoopLargeStructArrayU2(b *testing.B) {

 b.ReportAllocs()

 var arr [100000]U2

 for i := 0; i < b.N; i++ {

  for j := 0; j < len(arr)-1; j++ {

   arr[j].d = j

  }

 }

}

 

func BenchmarkClassicForLoopLargeStructArrayU1(b *testing.B) {

 b.ReportAllocs()

 var arr [100000]U1

 for i := 0; i < b.N; i++ {

  for j := 0; j < len(arr)-1; j++ {

   arr[j].d = j

  }

 }

}

 

func BenchmarkForRangeLargeStructArrayU5(b *testing.B) {

 b.ReportAllocs()

 var arr [100000]U5

 for i := 0; i < b.N; i++ {

  for j, v := range arr {

   arr[j].d = j

   _ = v

  }

 }

}

func BenchmarkForRangeLargeStructArrayU4(b *testing.B) {

 b.ReportAllocs()

 var arr [100000]U4

 for i := 0; i < b.N; i++ {

  for j, v := range arr {

   arr[j].d = j

   _ = v

  }

 }

}

 

func BenchmarkForRangeLargeStructArrayU3(b *testing.B) {

 b.ReportAllocs()

 var arr [100000]U3

 for i := 0; i < b.N; i++ {

  for j, v := range arr {

   arr[j].d = j

   _ = v

  }

 }

}

func BenchmarkForRangeLargeStructArrayU2(b *testing.B) {

 b.ReportAllocs()

 var arr [100000]U2

 for i := 0; i < b.N; i++ {

  for j, v := range arr {

   arr[j].d = j

   _ = v

  }

 }

}

func BenchmarkForRangeLargeStructArrayU1(b *testing.B) {

 b.ReportAllocs()

 var arr [100000]U1

 for i := 0; i < b.N; i++ {

  for j, v := range arr {

   arr[j].d = j

   _ = v

  }

 }

}

ログイン後にコピー

在这个例子中,我们定义了 5 种类型的结构体:U1~U5,它们的区别在于包含的 int 类型字段的数量。

性能测试结果如下

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

$ go test -bench . forRange2_test.go

goos: darwin

goarch: amd64

cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz

BenchmarkClassicForLoopLargeStructArrayU5-8        44540             26227 ns/op               0 B/op          0 allocs/op

BenchmarkClassicForLoopLargeStructArrayU4-8        45906             26312 ns/op               0 B/op          0 allocs/op

BenchmarkClassicForLoopLargeStructArrayU3-8        43315             27400 ns/op               0 B/op          0 allocs/op

BenchmarkClassicForLoopLargeStructArrayU2-8        44605             26313 ns/op               0 B/op          0 allocs/op

BenchmarkClassicForLoopLargeStructArrayU1-8        45752             26110 ns/op               0 B/op          0 allocs/op

BenchmarkForRangeLargeStructArrayU5-8               3072            388651 ns/op               0 B/op          0 allocs/op

BenchmarkForRangeLargeStructArrayU4-8               4605            261329 ns/op               0 B/op          0 allocs/op

BenchmarkForRangeLargeStructArrayU3-8               5857            182565 ns/op               0 B/op          0 allocs/op

BenchmarkForRangeLargeStructArrayU2-8              10000            108391 ns/op               0 B/op          0 allocs/op

BenchmarkForRangeLargeStructArrayU1-8              36333             32346 ns/op               0 B/op          0 allocs/op

PASS

ok      command-line-arguments  16.160s

ログイン後にコピー

我们看到一个现象:不管是什么类型的结构体元素数组,经典的 for 循环遍历的性能比较一致,但是 for range 的遍历性能会随着结构字段数量的增加而降低。

带着疑惑,发现了一个与这个问题相关的 issue:cmd/compile: optimize large structs:https://github.com/golang/go/issues/24416。这个 issue 大致是说:如果一个结构体类型有超过一定数量的字段(或一些其他条件),就会将该类型视为 unSSAable。如果 SSA 不可行,那么就无法通过 SSA 优化,这也是造成上述基准测试结果的重要原因。

结论

对于遍历大数组而言, for 循环能比 for range 循环更高效与稳定,这一点在数组元素为结构体类型更加明显。

另外,由于在 Go 中切片的底层都是通过数组来存储数据,尽管有 for range 的副本复制问题,但是切片副本指向的底层数组与原切片是一致的。这意味着,当我们将数组通过切片代替后,不管是通过 for range 或者 for 循环均能得到一致的稳定的遍历性能。

以上がGo で大きな配列を処理する: for 範囲または for ループを使用しますか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート