Что представляет собой сжатый способ создания 2D-фрагмента в Go?

Я учусь, пройдя Tour of Go. В одном из упражнений мне предлагается создать 2D-фрагмент строк dy и столбцов dx, содержащих uint8. Мой текущий подход, который работает, заключается в следующем:

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
}

Я думаю, что итерация через каждый срез для его инициализации слишком многословна. И если бы срез имел больше размеров, код стал бы громоздким. Есть ли сжатый способ инициализации 2D (или n-мерных) срезов в Go?

Ответ 1

Нет более сжатого способа, что вы сделали, это "правильный" способ; потому что срезы всегда одномерные, но могут быть составлены для построения объектов с большими размерами. См. Этот вопрос для получения дополнительной информации: Go: Как представление памяти двухмерного массива.

Одна вещь, которую вы можете упростить, заключается в использовании конструкции for range:

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

Также обратите внимание, что если вы инициализируете свой фрагмент с помощью составного литерала, вы получите это для "бесплатного", например:

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, его можно создать очень легко:

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]]

В случае массивов вам не нужно перебирать "внешний" массив и инициализировать "внутренние" массивы, поскольку массивы не являются дескрипторами, а значениями. См. Сообщение в блоге Массивы, срезы (и строки): Механика "append" для более подробной информации.

Попробуйте примеры на Go Playground.

Ответ 2

Есть два способа использования срезов для создания матрицы. Давайте посмотрим на различия между ними.

Первый метод:

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

Второй метод:

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

Что касается первого метода, выполнение последовательных вызовов make не гарантирует, что у вас будет непрерывная матрица, поэтому вы можете разделить матрицу в памяти. Давайте рассмотрим пример с двумя подпрограммами Go, которые могут вызвать это:

  1. Подпрограмма # 0 запускает make([][]int, n) чтобы получить выделенную память для matrix, получая часть памяти от 0x000 до 0x07F.
  2. Затем он запускает цикл и выполняет первую строку make([]int, m), получая значение от 0x080 до 0x0FF.
  3. Во второй итерации планировщик вытесняет ее.
  4. Планировщик передает процессор подпрограмме # 1, и он начинает работать. Этот также использует make (для своих собственных целей) и получает от 0x100 до 0x17F (прямо рядом с первым рядом подпрограммы # 0).
  5. Через некоторое время он прерывается, и процедура # 0 снова запускается.
  6. Он выполняет make([]int, m) соответствующую второй итерации цикла, и получает значение от 0x180 до 0x1FF для второй строки. На данный момент мы уже получили два разделенных ряда.

Во втором методе подпрограмма make([]int, n*m) получает всю матрицу, выделенную в одном срезе, обеспечивая непрерывность. После этого необходим цикл для обновления указателей матрицы на субликаты, соответствующие каждой строке.

Вы можете поиграть с кодом, показанным выше, на игровой площадке Go, чтобы увидеть разницу в памяти, назначенной с помощью обоих методов. Обратите внимание, что я использовал runtime.Gosched() только с целью получения процессора и принудительного переключения планировщика на другую подпрограмму.

Какой использовать? Представьте себе наихудший случай с первым методом, то есть каждая строка не находится в памяти рядом с другой строкой. Затем, если ваша программа выполняет итерации по элементам матрицы (для чтения или записи), вероятно, будет больше пропусков кэша (а следовательно, и более высокая задержка) по сравнению со вторым методом из-за худшей локальности данных. С другой стороны, при использовании второго метода может оказаться невозможным получить один фрагмент памяти, выделенный для матрицы, из-за фрагментации памяти (фрагменты распределяются по всей памяти), хотя теоретически для этого может быть достаточно свободной памяти,

Поэтому, если нет большой фрагментации памяти и матрицы, которая будет выделена, достаточно велика, вы всегда захотите использовать второй метод, чтобы получить преимущество от локальности данных.

Ответ 3

Вы можете ссылаться на этот фрагмент кода -

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.
}

Ссылка

Ответ 4

Я написал эту функцию для замены строк двумерного массива:

func lchange(l1 int, l2 int, a [][]float64) {
    b := make([]float64, 1)
    b = a[l1][:]
    a[l1] = a[l2]
    a[l2] = b
}