web-dev-qa-db-ja.com

Goで2Dスライスを作成する簡潔な方法は何ですか?

A Tour of Go を実行してGoを学習しています。演習の1つでは、uint8を含むdy行とdx列の2Dスライスを作成するように求められます。うまくいく私の現在のアプローチはこれです:

a:= make([][]uint8, dy)       // initialize a slice of dy slices
for i:=0;i<dy;i++ {
    a[i] = make([]uint8, dx)  // initialize a slice of dx unit8 in each of dy slices
}

各スライスを繰り返して初期化するのは冗長すぎると思います。また、スライスの次元が増えると、コードが扱いにくくなります。 Goで2D(またはn次元)スライスを初期化する簡潔な方法はありますか?

57
hazrmard

もっと簡潔な方法はありません。あなたがしたことは「正しい」方法です。スライスは常に1次元ですが、より高次元のオブジェクトを構築するために構成できるためです。詳細については、この質問を参照してください: Go:2次元配列のメモリ表現はどうですか

簡単にできることの1つは、for rangeコンストラクトを使用することです。

a := make([][]uint8, dy)
for i := range a {
    a[i] = make([]uint8, dx)
}

また、スライスを composite literal で初期化すると、「無料」でこれを取得できることに注意してください。例:

a := [][]uint8{
    {0, 1, 2, 3},
    {4, 5, 6, 7},
}
fmt.Println(a) // Output is [[0 1 2 3] [4 5 6 7]]

はい、すべての要素を列挙する必要があるように見えるため、これには制限があります。ただし、いくつかのトリックがあります。つまり、すべての値を列挙する必要はなく、スライスの要素タイプの ゼロ値 でない値のみを列挙する必要があります。この詳細については、 golang配列初期化のキー付きアイテム を参照してください。

たとえば、最初の10個の要素がゼロで、1および2に続くスライスが必要な場合、次のように作成できます。

b := []uint{10: 1, 2}
fmt.Println(b) // Prints [0 0 0 0 0 0 0 0 0 0 1 2]

また、 slices の代わりに arrays を使用する場合、非常に簡単に作成できることに注意してください。

c := [5][5]uint8{}
fmt.Println(c)

出力は次のとおりです。

[[0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0]]

配列の場合、配列は記述子ではなく値であるため、「外部」配列を反復処理して「内部」配列を初期化する必要はありません。詳細については、ブログ投稿 配列、スライス(および文字列):「追加」の仕組み を参照してください。

Go Playground の例を試してください。

90
icza

スライスを使用してマトリックスを作成するには、2つの方法があります。それらの違いを見てみましょう。

最初の方法:

matrix := make([][]int, n)
for i := 0; i < n; i++ {
    matrix[i] = make([]int, m)
}

2番目の方法:

matrix := make([][]int, n)
rows := make([]int, n*m)
for i := 0; i < n; i++ {
    matrix[i] = rows[i*m : (i+1)*m]
}

最初の方法に関して、連続してmake呼び出しを行っても、連続したマトリックスになることが保証されないため、マトリックスをメモリに分割することができます。これを引き起こす可能性のある2つのGoルーチンの例を考えてみましょう。

  1. ルーチン#0はmake([][]int, n)を実行してmatrixに割り当てられたメモリを取得し、0x000から0x07Fのメモリを取得します。
  2. 次に、ループを開始し、最初の行make([]int, m)を実行して、0x080から0x0FFまでを取得します。
  3. 2回目の反復では、スケジューラによってプリエンプトされます。
  4. スケジューラーはプロセッサーをルーチン#1に渡し、実行を開始します。これもmake(独自の目的で)を使用し、0x100から0x17F(ルーチン#0の最初の行のすぐ隣)から取得します。
  5. しばらくすると、プリエンプトされ、ルーチン#0が再び実行を開始します。
  6. 2番目のループの反復に対応するmake([]int, m)を実行し、2行目の0x180から0x1FFを取得します。この時点で、すでに2つの分割された行があります。

2番目の方法では、ルーチンはmake([]int, n*m)を実行して、単一スライスに割り当てられたすべてのマトリックスを取得し、連続性を確保します。その後、各行に対応するサブスライスへのマトリックスポインターを更新するループが必要です。

Go Playground で上記のコードを再生して、両方の方法を使用して割り当てられたメモリの違いを確認できます。 runtime.Gosched()を使用したのは、プロセッサを譲り、スケジューラを別のルーチンに強制的に切り替える目的でのみ使用したことに注意してください。

どちらを使用しますか?最初の方法で最悪のケースを想像してください。つまり、各行がメモリ内で別の行に隣接していないということです。次に、プログラムが(読み取りまたは書き込みのために)マトリックス要素を反復処理する場合、2番目の方法と比較して、データの局所性が悪いため、おそらくキャッシュミスが多くなります(したがって、レイテンシが高くなります)。一方、2番目の方法では、理論的には十分な空きメモリがある場合でも、メモリの断片化(チャンクがメモリ全体に広がる)のために、マトリックスに割り当てられた単一のメモリを取得できない場合があります。

したがって、メモリの断片化が多く、割り当てられるマトリックスが十分に大きい場合を除き、常に2番目の方法を使用してデータの局所性を利用することをお勧めします。

5

このコードを参照できます-

package main

import "fmt"

func main() {
    var row, col int
    fmt.Print("enter rows cols: ")
    fmt.Scan(&row, &col)

    // allocate composed 2d array
    a := make([][]int, row)
    for i := range a {
        a[i] = make([]int, col)
    }

    // array elements initialized to 0
    fmt.Println("a[0][0] =", a[0][0])

    // assign
    a[row-1][col-1] = 7

    // retrieve
    fmt.Printf("a[%d][%d] = %d\n", row-1, col-1, a[row-1][col-1])

    // remove only reference
    a = nil
    // memory allocated earlier with make can now be garbage collected.
}

参照

0
Techidiot