วิธีที่กระชับในการสร้างชิ้นงาน 2 มิติใน Go คืออะไร?


111

ฉันเรียนรู้ไปโดยจะผ่านทัวร์ของไป หนึ่งของการออกกำลังกายมีถามฉันเพื่อสร้างชิ้น 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
}

ฉันคิดว่าการทำซ้ำในแต่ละชิ้นเพื่อเริ่มต้นมันดูละเอียดเกินไป และถ้าชิ้นส่วนมีขนาดมากขึ้นโค้ดก็จะดูเทอะทะ มีวิธีที่กระชับในการเริ่มต้นชิ้นงาน 2 มิติ (หรือ n มิติ) ใน Go หรือไม่

คำตอบ:


165

ไม่มีวิธีที่กระชับกว่านี้สิ่งที่คุณทำคือวิธีที่ "ถูกต้อง" เนื่องจากชิ้นส่วนเป็นมิติเดียวเสมอ แต่อาจประกอบขึ้นเพื่อสร้างวัตถุที่มีมิติสูงขึ้น ดูคำถามนี้สำหรับรายละเอียดเพิ่มเติม: ไป: การแสดงหน่วยความจำของอาร์เรย์สองมิติเป็นอย่างไร

สิ่งหนึ่งที่คุณสามารถทำให้มันง่ายขึ้นคือการใช้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]

นอกจากนี้โปรดทราบว่าหากคุณใช้อาร์เรย์แทนชิ้นสามารถสร้างได้ง่ายมาก:

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

ในกรณีของอาร์เรย์คุณไม่จำเป็นต้องวนซ้ำบนอาร์เรย์ "ด้านนอก" และเริ่มต้นอาร์เรย์ "ภายใน" เนื่องจากอาร์เรย์ไม่ใช่ตัวบ่งชี้ แต่เป็นค่า ดูบล็อกโพสต์อาร์เรย์ชิ้นส่วน (และสตริง): กลไกของ "ผนวก"สำหรับรายละเอียดเพิ่มเติม

ลองตัวอย่างบนไปสนามเด็กเล่น


เนื่องจากการใช้อาร์เรย์ทำให้โค้ดง่ายขึ้นฉันจึงต้องการทำเช่นนั้น เราระบุสิ่งนั้นในโครงสร้างได้อย่างไร? ฉันได้รับcannot use [5][2]string literal (type [5][2]string) as type [][]string in field valueเมื่อฉันพยายามกำหนดอาร์เรย์ให้กับสิ่งที่ฉันเดาว่าฉันกำลังบอกว่า Go เป็นสไลซ์
Eric Lindsey

คิดออกเองและแก้ไขคำตอบเพื่อเพิ่มข้อมูล
Eric Lindsey

1
@EricLindsey แม้ว่าการแก้ไขของคุณจะดี แต่ฉันก็ยังคงปฏิเสธเพราะฉันไม่ต้องการสนับสนุนให้ใช้อาร์เรย์เพียงเพราะการเริ่มต้นทำได้ง่ายกว่า ใน Go อาร์เรย์เป็นตัวรองส่วนสไลซ์เป็นวิธีที่จะไป สำหรับรายละเอียดโปรดดูวิธีที่เร็วที่สุดในการผนวกอาร์เรย์หนึ่งเข้ากับอีกอาร์เรย์หนึ่งใน Go คืออะไร อาร์เรย์ก็มีตำแหน่งเช่นกันสำหรับรายละเอียดโปรดดูเหตุใดจึงมีอาร์เรย์ใน Go
icza

ยุติธรรมพอ แต่ฉันเชื่อว่าข้อมูลยังคงมีประโยชน์ สิ่งที่ฉันพยายามอธิบายด้วยการแก้ไขของฉันคือถ้าคุณต้องการความยืดหยุ่นของมิติที่แตกต่างกันระหว่างวัตถุชิ้นส่วนก็เป็นหนทางที่จะไป ในทางกลับกันหากข้อมูลของคุณมีโครงสร้างที่เข้มงวดและมักจะเหมือนกันอาร์เรย์ไม่เพียง แต่จะเริ่มต้นได้ง่ายขึ้นเท่านั้น แต่ยังมีประสิทธิภาพมากกว่าอีกด้วย ฉันจะปรับปรุงการแก้ไขได้อย่างไร
Eric Lindsey

@EricLindsey ฉันเห็นว่าคุณแก้ไขอีกครั้งซึ่งคนอื่นปฏิเสธไปแล้ว ในการแก้ไขของคุณคุณกำลังบอกว่าจะใช้อาร์เรย์เพื่อให้เข้าถึงองค์ประกอบได้เร็วขึ้น โปรดทราบว่า Go เพิ่มประสิทธิภาพหลาย ๆ อย่างและอาจไม่เป็นเช่นนั้นชิ้นส่วนอาจเร็วเท่า ดูรายละเอียดอาร์เรย์ VS Slice: ความเร็วในการเข้าถึง
icza

14

มีสองวิธีในการใช้ชิ้นส่วนเพื่อสร้างเมทริกซ์ ลองมาดูความแตกต่างระหว่างกัน

วิธีแรก:

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 Playgroundเพื่อดูความแตกต่างของหน่วยความจำที่กำหนดโดยใช้ทั้งสองวิธี โปรดทราบว่าฉันใช้runtime.Gosched()โดยมีจุดประสงค์เพื่อให้โปรเซสเซอร์และบังคับให้ตัวกำหนดตารางเวลาเปลี่ยนไปใช้รูทีนอื่นเท่านั้น

จะใช้อันไหนดี? ลองนึกภาพกรณีที่เลวร้ายที่สุดด้วยวิธีแรกนั่นคือแต่ละแถวไม่อยู่ถัดจากหน่วยความจำไปยังแถวอื่น จากนั้นหากโปรแกรมของคุณวนซ้ำผ่านองค์ประกอบเมทริกซ์ (เพื่ออ่านหรือเขียน) อาจมีการพลาดแคชมากขึ้น (ดังนั้นเวลาแฝงที่สูงกว่า) เมื่อเทียบกับวิธีที่สองเนื่องจากตำแหน่งของข้อมูลแย่ลง ในทางกลับกันด้วยวิธีที่สองอาจเป็นไปไม่ได้ที่จะได้รับหน่วยความจำชิ้นเดียวที่จัดสรรให้กับเมทริกซ์เนื่องจากการกระจายตัวของหน่วยความจำ (ชิ้นส่วนที่กระจายไปทั่วหน่วยความจำ) แม้ว่าในทางทฤษฎีแล้วอาจมีหน่วยความจำว่างเพียงพอสำหรับมัน .

ดังนั้นหากไม่มีการแยกส่วนหน่วยความจำจำนวนมากและเมทริกซ์ที่จะจัดสรรนั้นมีขนาดใหญ่พอคุณก็มักจะต้องการใช้วิธีที่สองเพื่อให้ได้ประโยชน์จากพื้นที่ข้อมูล


2
golang.org/doc/effective_go.html#slicesแสดงวิธีที่ชาญฉลาดในการใช้เทคนิคหน่วยความจำที่ต่อเนื่องกันโดยใช้ประโยชน์จากไวยากรณ์แบบสไลซ์เนทีฟ (เช่นไม่จำเป็นต้องคำนวณขอบเขตของสไลซ์อย่างชัดเจนด้วยนิพจน์เช่น (i + 1) * m)
Magnus
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.