Jaki jest zwięzły sposób tworzenia wycinka 2D w Go?

108

Uczę się Go, przechodząc przez A Tour of Go . Jedno z ćwiczeń prosi mnie o utworzenie wycinka 2D zawierającego dywiersze i dxkolumny uint8. Moje obecne podejście, które działa, jest następujące:

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
}

Myślę, że iterowanie po każdym wycinku w celu jego zainicjowania jest zbyt szczegółowe. A gdyby wycinek miał więcej wymiarów, kod stałby się nieporęczny. Czy istnieje zwięzły sposób inicjalizacji wycinków 2D (lub n-wymiarowych) w Go?

hazrmard
źródło

Odpowiedzi:

158

Nie ma bardziej zwięzłego sposobu, to, co zrobiłeś, jest „właściwą” drogą; ponieważ plasterki są zawsze jednowymiarowe, ale mogą być składane w celu konstruowania obiektów o wyższych wymiarach. Zobacz to pytanie po więcej szczegółów: Idź: Jak jest reprezentacja pamięci w dwuwymiarowej tablicy .

Jedną rzeczą, którą możesz uprościć, jest użycie for rangekonstrukcji:

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

Zwróć też uwagę, że jeśli zainicjujesz swój plasterek literałem złożonym , otrzymasz to za darmo, na przykład:

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

Tak, to ma swoje ograniczenia, ponieważ pozornie musisz wymienić wszystkie elementy; ale są pewne sztuczki, mianowicie nie musisz wyliczać wszystkich wartości, tylko te, które nie są zerowymi wartościami typu elementu wycinka. Aby uzyskać więcej informacji na ten temat, zobacz Keyed items in golang array initialization .

Na przykład, jeśli chcesz plasterek, w którym pierwsze 10 elementów to zera, a następnie następuje 1i 2, można go utworzyć w następujący sposób:

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

Pamiętaj również, że jeśli zamiast plasterków użyjesz tablic , możesz je bardzo łatwo utworzyć:

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

Wynik to:

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

W przypadku tablic nie musisz iterować po tablicy „zewnętrznej” i inicjować tablic „wewnętrznych”, ponieważ tablice nie są deskryptorami, ale wartościami. Zobacz wpis na blogu Tablice, plasterki (i ciągi): Mechanika „dołączania”, aby uzyskać więcej informacji.

Wypróbuj przykłady w Go Playground .

icza
źródło
Ponieważ użycie tablicy upraszcza kod, chciałbym to zrobić. Jak można to określić w strukturze? Dostaję, cannot use [5][2]string literal (type [5][2]string) as type [][]string in field valuekiedy próbuję przypisać tablicę do tego, co, jak sądzę, mówię, że Go to kawałek.
Eric Lindsey
Sam to rozgryzłem i zredagowałem odpowiedź, aby dodać informacje.
Eric Lindsey
1
@EricLindsey Chociaż twoja edycja jest dobra, nadal ją odrzucę, ponieważ nie chcę zachęcać do używania tablic tylko dlatego, że inicjalizacja jest łatwiejsza. W Go tablice są drugorzędne, a plasterki są do zrobienia. Aby uzyskać szczegółowe informacje, zobacz Jaki jest najszybszy sposób dołączania jednej tablicy do drugiej w Go? Tablice też mają swoje miejsca, aby uzyskać szczegółowe informacje, zobacz Dlaczego mają tablice w Go?
icza
dość uczciwe, ale uważam, że informacje nadal mają wartość. W mojej edycji starałem się wyjaśnić, że jeśli potrzebujesz elastyczności różnych wymiarów między obiektami, to najlepszym rozwiązaniem są plasterki. Z drugiej strony, jeśli Twoje informacje mają sztywną strukturę i zawsze będą takie same, to tablice są nie tylko łatwiejsze do zainicjowania, ale są również bardziej wydajne. Jak mogę poprawić edycję?
Eric Lindsey
@EricLindsey Widzę, że dokonałeś kolejnej edycji, która została już odrzucona przez innych. W swojej edycji mówiłeś, że chcesz używać tablic, aby mieć szybszy dostęp do elementów. Zauważ, że Go optymalizuje wiele rzeczy i może tak nie być, plasterki mogą być równie szybkie. Aby uzyskać szczegółowe informacje, zobacz Array vs Slice: accessing speed .
icza
12

Istnieją dwa sposoby tworzenia macierzy za pomocą plasterków. Przyjrzyjmy się różnicom między nimi.

Pierwsza metoda:

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

Druga metoda:

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

Jeśli chodzi o pierwszą metodę, wykonywanie kolejnych makewywołań nie gwarantuje, że otrzymasz ciągłą macierz, więc możesz mieć macierz podzieloną w pamięci. Pomyślmy o przykładzie z dwiema procedurami Go, które mogą to spowodować:

  1. Procedura # 0 jest uruchamiana, make([][]int, n)aby uzyskać przydzieloną pamięć matrix, uzyskując fragment pamięci od 0x000 do 0x07F.
  2. Następnie rozpoczyna pętlę i wykonuje pierwszy wiersz make([]int, m), przechodząc od 0x080 do 0x0FF.
  3. W drugiej iteracji zostaje wywłaszczony przez program planujący.
  4. Program planujący przekazuje procesorowi procedurę nr 1 i zaczyna działać. Ten również używa make(do własnych celów) i pobiera od 0x100 do 0x17F (tuż obok pierwszego wiersza procedury # 0).
  5. Po chwili zostaje wywłaszczony i procedura # 0 zaczyna ponownie działać.
  6. Robi to, co make([]int, m)odpowiada drugiej iteracji pętli i pobiera od 0x180 do 0x1FF dla drugiego rzędu. W tym momencie mamy już dwa podzielone rzędy.

W przypadku drugiej metody procedura polega make([]int, n*m)na przydzieleniu całej macierzy w jednym wycinku, zapewniając ciągłość. Następnie potrzebna jest pętla, aby zaktualizować wskaźniki macierzy do podklasów odpowiadających każdemu wierszowi.

Możesz pobawić się kodem pokazanym powyżej w Go Playground, aby zobaczyć różnicę w pamięci przypisanej za pomocą obu metod. Zauważ, że użyłem runtime.Gosched()tylko w celu uzyskania procesora i zmuszenia planisty do przełączenia się na inną procedurę.

Którego użyć? Wyobraź sobie najgorszy przypadek w przypadku pierwszej metody, tj. Każdy wiersz nie jest w pamięci następny. Następnie, jeśli twój program iteruje przez elementy macierzy (aby je odczytać lub zapisać), prawdopodobnie będzie więcej błędów pamięci podręcznej (stąd większe opóźnienie) w porównaniu z drugą metodą z powodu gorszej lokalizacji danych. Z drugiej strony, w przypadku drugiej metody może nie być możliwe uzyskanie pojedynczego fragmentu pamięci przydzielonej do macierzy, ze względu na fragmentację pamięci (porcje rozrzucone po całej pamięci), mimo że teoretycznie może być na to dość wolnej pamięci .

Dlatego jeśli nie ma dużej fragmentacji pamięci, a macierz, która ma być przydzielona, ​​nie jest wystarczająco duża, zawsze chciałbyś użyć drugiej metody, aby uzyskać przewagę lokalności danych.

Marcos Canales Mayo
źródło
2
golang.org/doc/effective_go.html#slices pokazuje sprytny sposób na wykonanie techniki pamięci ciągłej, wykorzystującej składnię natywną dla wycinka (np. nie ma potrzeby jawnego obliczania granic wycinka za pomocą wyrażeń takich jak (i ​​+ 1) * m)
Magnus,