มีอะไรใหม่ใน .NET Core 3 และ C# 8 : Stackalloc ซ้อนนิพจน์
Stackalloc ซ้อนนิพจน์
เริ่มตั้งแต่ C# 8 และ .NET Core 3.0 ถ้าผลลัพธ์ของนิพจน์ stackalloc มีชนิดข้อมูลเป็นแบบ System.Span<T> หรือ System.ReadOnlySpan<T> เราสามารถใส่นิพจน์ stackalloc ซ้อนไว้ภายในนิพจน์อื่น ๆ ได้
รูปที่ 1 แสดงตัวอย่างโค้ดการใช้นิพจน์ stackalloc ซ้อนไว้ภายในนิพจน์อื่น ๆ
บรรทัด 9,10 แสดงตัวอย่างการใช้นิพจน์ stackalloc แบบเดี่ยว ๆ ไม่ได้ซ้อนอยู่ในนิพจน์ใด
บรรทัด 12,13 แสดงตัวอย่างการใช้นิพจน์ stackalloc ที่ซ้อนอยู่ภายในนิพจน์อื่น วงเล็บหลัง method IndexOfAny คือที่ใส่ argument ที่จะเป็นนิพจน์ก็ได้ และในที่นี้เป็นนิพจน์ stackalloc
บรรทัด 15 ผลลัพธ์การทำงานของโปรแกรมนี้คือ 1
รูปที่ 1

ตัวกระทำ stackalloc
stackalloc คือตัวกระทำในภาษา C#
ทำหน้าที่จองหน่วยความจำใน Stack ก้อนของหน่วยความจำที่เกิดจากการจองหน่วยความจำใน Stack ขณะที่ method ทำงาน จะถูกกำจัดทิ้งไปโดยอัตโนมัติเมื่อ method จบการทำงานลง
เราไม่อาจทำลายก้อนของหน่วยความจำที่เกิดจากการจองหน่วยความจำใน Stack ที่เกิดจากตัวกระทำ stackalloc เองได้
ก้อนของหน่วยความจำที่เกิดจากการจองนี้ จะไม่ได้รับผลกระทบจากการทำงานของผู้เก็บขยะ (GC) และจะไม่ถูกตรึงไว้ด้วยคำสั่ง fixed
ในนิพจน์ stackalloc T[E] ตัว T ต้องเป็น object ที่เป็นชนิดไม่ถูกคุม
(unmanaged type หมายถึงไม่ได้ถูกดูแลจัดการโดยตัว run time CLR)
ส่วนตัว E จะต้องเป็นนิพจน์ที่มี type เป็น int
เราอาจนำค่าจากผลลัพธ์ของนิพจน์ stackalloc ไปใส่ไว้ในตัวแปรที่มี type แบบใดแบบหนึ่งดังต่อไปนี้
System.Span<T>
System.ReadOnlySpan<T>
Span<T>
ReadOnlySpan<T>
นำไปใช้เป็นนิพจน์ (เริ่มใน .NET Core 3.0)
ตัวแปรแบบ pointer
เราไม่จำเป็นต้องใช้คำสั่ง unsafe เมื่อใช้ตัวกระทำ stackalloc เพื่อจองหน่วยความจำใน Stack
และไม่จำเป็นต้องนำผลที่ได้ไปเก็บในตัวแปรแบบ Span<T> หรือ ReadOnlySpan<T>
และเราอาจใช้นิพจน์ stackalloc เพื่อจองหน่วยความจำอย่างมีเงื่อนไขได้
รูปที่ 2 แสดงตัวอย่างโค้ดการใช้งานนิพจน์ stackalloc
บรรทัด 9 ประกาศตัวแปรไว้ทำหน้าที่กำหนดขนาดพื้นที่ของหน่วยความจำที่ต้องการจองไว้ใน Stack
บรรทัด 10 จองหน่วยความจำใน Stack มีขนาดตามตัวแปร length
บรรทัด 13 สาธิตวิธีนำค่าเข้าไปเก็บในหน่วยความจำใน Stack ที่จองไว้
บรรทัด 17-20 สาธิตวิธีใช้นิพจน์ stackalloc เพื่อจองหน่วยความจำอย่างมีเงื่อนไข ตัวอย่างที่เห็นนี้คือถ้าพื้นที่มีขนาดเล็กกว่า 1,024 ให้ใช้ตัวกระทำ stackalloc เพื่อจองหน่วยความจำใน Stack
รูปที่ 2

ตัวแปรแบบ pointer กับ stackalloc
เราอาจใช้ตัวแปรแบบ pointer เพื่ออ้างถึงพื้นที่ในหน่วยความจำที่เราจองไว้โดยใช้ตัวกระทำ stackalloc ได้อย่างที่เห็นในรูปที่ 3
ซึ่งแสดงโค้ดตัวอย่างการใช้งานนิพจน์ stackalloc กับตัวแปรแบบ pointer
เนื่องจากมี pointer เข้ามาเกี่ยวข้องจึงทำเป็นต้องให้โค้ดทั้งหมดอยู่ในบล็อก unsafe
ซึ่งหมายความว่าตัว run time CLR จะไม่คอยตรวจสอบสิ่งต่าง ๆ ในบล็อกนั้น
อาทิ ไม่ตรวจสอบขอบเขตของ array ถ้าโค้ดของเราอ้างถึงหน่วยของ array ที่เกินค่าดรรชนีจะไม่เกิด run time exception แต่โปรแกรมจะค้าง
บรรทัด 9-15 แสดงตัวอย่างโค้ดการใช้งานนิพจน์ stackalloc ที่ไม่ได้เรียกใช้ตัวแปรแบบ pointer
บรรทัด 17-24 บล็อก unsafe run time CLR จะไม่ตรวจสอบสิ่งต่าง ๆ ในบล็อกนี้
บรรทัด 20 ประกาศตัวแปร pointer เพื่อใช้อ้างถึงพื้นที่ในหน่วยความจำที่เราจองไว้โดยใช้ตัวกระทำ stackalloc
บรรทัด 23 การกำหนดค่าให้แก่พื้นที่ในหน่วยความจำที่เราจองไว้โดยใช้ pointer
บรรทัด 27 โดยปริยายพื้นที่ในหน่วยความจำที่เราจองไว้โดยใช้ตัวกระทำ stackalloc จะไม่แน่ชัดว่ามีอะไรเก็บอยู่ โค้ดนี้สาธิตการจองพื้นที่ในหน่วยความจำและกำหนดค่าเริ่มต้นโดยกำหนดให้เป็นเลขจำนวนเต็ม และระบุขนาดว่าเป็นสามหน่วย
บรรทัด 30 โค้ดนี้สาธิตการจองพื้นที่ในหน่วยความจำและกำหนดค่าเริ่มต้นโดยไม่ได้ระบุขนาด แต่พื้นที่จะมีขนาดเท่ากับข้อมูลที่เรากำหนดเป็นค่าเริ่มต้นคือสามหน่วย
บรรทัด 33 โค้ดนี้สาธิตการจองพื้นที่ในหน่วยความจำแบบให้อ่านได้เท่านั้นและกำหนดค่าเริ่มต้นโดยไม่ได้ระบุขนาด แต่พื้นที่จะมีขนาดเท่ากับข้อมูลที่เรากำหนดเป็นค่าเริ่มต้นคือสามหน่วย
รูปที่ 3

Span กับ array
เนื่องจากตัวกระทำ stackalloc จะจองหน่วยความจำใน Stack ซึ่งเป็นพื้นที่ ๆ ต่อเนื่องเป็นผืนเดียวกัน
เราจึงอาจอ้างอิงถึงมันโดยใช้ type Span ได้ เพราะ type Span ถูกสร้างมาเพื่อการนี้โดยตรง
type Span ถูกออกแบบมาให้ทำงานกับพื้นที่ต่อเนื่องใน Stack (ไม่ใช่ใน heap) และมีกลไกป้องกันไม่ให้นำไปใช้กับอย่างอื่นนอกจาก Stack
type Span<T> ช่วยให้เราอ้างถึงพื้นที่ต่อเนื่องที่กำหนดขึ้นเองได้ ปกติจะเป็น array หรือบางส่วนของ array
โดยทั่วไปแล้วตัว run time จะจองหน่วยความจำของ array ให้เป็นพื้นที่ต่อเนื่องกันเฉพาะของ array นั้น
ขณะที่เราสามารถใช้ Span อ้างถึงพื้นที่ควบคุม พื้นที่ native และพื้นที่ควบคุมภายใน Stack ได้
รูปที่ 4 แสดงโค้ดตัวอย่างการสร้าง Span กับพื้นที่ใน array
บรรทัด 10 ประกาศ array ขนาดหนึ่งร้อย byte
บรรทัด 11 สร้าง span ครอบ array
บรรทัด 13 ประกาศตัวแปรเพื่อเก็บค่าที่จะนำไปถมใน span
บรรทัด 14-15 วนการทำงานเพื่อกำหนดค่าให้แก่แต่ละหน่วยของ span (การถมค่าเข้าไปใน span)
บรรทัด 17 ประกาศตัวแปรเพื่อไว้ใช้เก็บค่าผลรวม
บรรทัด 18-19 วนการทำงานเพื่ออ่านค่าจาก array และคำนวณผลรวม
บรรทัด 21 แสดงผลรวม
รูปที่ 4

Span กับหน่วยความจำที่กำหนด
method AllocHGlobal ที่อยู่ภายในคลาส Marshal มีไว้เพื่อจองเนื้อที่แบบไม่คุม (unmanaged) ภายในหน่วยความจำของ processได้โดยการระบุจำนวน byte ที่ต้องการ
ค่าส่งกลับของ method นี้คือ pointer ที่ชี้ไปยังหน่วยความจำนี้
ข้อควรระวังคือเมื่อใช้งานเสร็จแล้วเราจะต้องปล่อยมันด้วย method FreeHGlobal ที่อยู่ภายในคลาส Marshal เช่นเดียวกัน
รูปที่ 5 แสดงโค้ดตัวอย่างการสร้าง Span กับพื้นที่ในหน่วยความจำที่กำหนด
บรรทัด 28 จองพื้นที่ในหน่วยความจำขนาดหนึ่งร้อยไบต์
บรรทัด 33 สร้าง span ครอบพื้นที่ในหน่วยความจำ
บรรทัด 36 ตัวแปรที่เก็บค่าที่จะนำไปถมใน span
บรรทัด 37-38 วนการทำงานเพื่อกำหนดค่าให้แก่แต่ละหน่วยของ span (ถมค่าเข้าไปใน span)
บรรทัด 40 ประกาศตัวแปรเก็บค่าผลรวม
บรรทัด 41-42 วนการทำงานเพื่ออ่านค่าจาก array และคำนวณผลรวม
บรรทัด 44 แสดงผลรวม
บรรทัด 45 ปล่อยการจองหน่วยความจำด้วย method FreeHGlobal
รูปที่ 5

Span กับ Stack
เมื่อต้องการทำ Span กับ Stackเราจะต้องจองหน่วยความจำใน Stack โดยใช้ตัวกระทำ stackalloc เสียก่อน
ส่วนการสร้างและใช้งาน Span จะไม่ต่างจากการใช้งาน Span กับ array และงาน Span กับพื้นที่ในหน่วยความจำที่กำหนดเองใน 2 หัวข้อที่ผ่านมา
รูปที่ 6 แสดงโค้ดตัวอย่างการสร้าง Span กับพื้นที่ ๆ จองไว้ใน Stack
บรรทัด 51 ตัวแปรที่เก็บค่าที่จะนำไปถมใน span
บรรทัด 52 จองพื้นที่ใน Stackขนาดหนึ่งร้อยไบต์ และสร้าง span ครอบพื้นที่นั้น
บรรทัด 53-54 วนการทำงานเพื่อกำหนดค่าให้แก่แต่ละหน่วยของ span (ถมค่าเข้าไปใน span)
บรรทัด 56 ประกาศตัวแปรเก็บค่าผลรวม
บรรทัด 57-58 วนการทำงานเพื่ออ่านค่าจาก array และคำนวณผลรวม
บรรทัด 60 แสดงผลรวม
รูปที่ 6

Span กับหน่วยความจำสามแบบ
เนื่องจากการใช้งาน Span<T> มีลักษณะเหมือนกันหมด ไม่ว่า T จะเป็น array Stack หรือพื้นที่ในหน่วยความจำที่กำหนดขึ้นเอง
ดังนั้นหากเราต้องการรวมโค้ด 3 ตัวอย่างที่ผ่านมาให้กลายเป็นโปรแแกรมเดียว
เราไม่จำเป็นต้องแยกโค้ดส่วนการกำหนดและส่วนแสดงค่าแยกเป็นหนึ่งแบบสำหรับหน่วยความจำแต่ละแบบ
แต่เราสามารถทำนิยาม method เดียวที่้ใช้งานร่วมกันได้ทั้งสามแบบซึ่งจะทำให้โค้ดสั้นลง
รูปที่ 7 คือโค้ดตัวอย่างการสร้างและใช้งาน Span กับหน่วยความจำทั้งสามแบบ
บรรทัด 10-13 แสดงการทำงานกับ array
บรรทัด 14-22 แสดงการทำงานกับพื้นที่ในหน่วยความจำที่กำหนดขึ้นเอง
บรรทัด 23-25 แสดงการทำงานกับ Stack
บรรทัด 10 ประกาศ array ขนาดหนึ่งร้อยไบต์
บรรทัด 11 สร้าง span ครอบ array
บรรทัด 12 เรียก method InitializeSpan ซึ่งทำหน้าที่กำหนดค่าให้แก่แต่ละหน่วยของ span (ถมค่าเข้าไปใน span)
บรรทัด 13 เรียก method ComputeSum ซึ่งทำหน้าที่คำนวณผลรวมแล้วแสดงค่าผลรวม
บรรทัด 14 ประกาศจองพื้นที่ในหน่วยความจำขนาดหนึ่งร้อยไบต์
บรรทัด 18 สร้าง span ครอบพื้นที่ในหน่วยความจำ
บรรทัด 20 เรียก method InitializeSpan ซึ่งทำหน้าที่กำหนดค่าให้แก่แต่ละหน่วยของ span (ถมค่าเข้าไปใน span)
บรรทัด 21 เรียก method ComputeSum ซึ่งทำหน้าที่คำนวณผลรวมแล้วแสดงค่าผลรวม
บรรทัด 22 ปล่อยการจองพื้นที่ในหน่วยความจำ
รูปที่ 7

บทความนี้ได้กล่าวถึงคุณสมบัติใหม่ในภาษา C# 8 และ .NET Core 3 ที่เกี่ยวข้องกับ
stackalloc ซ้อนนิพจน์,
ตัวกระทำ stackalloc,
ตัวแปรแบบ pointer กับ stackalloc,
Span กับ array ,
Span กับหน่วยความจำที่กำหนด,
Span กับ Stack และ Span กับหน่วยความจำทั้งสามแบบ
อย่างไรท่านผู้อ่านทดลองเขียนโค้ดดูเพื่อทดสอบคุณสมบัติใหม่ดังกล่าวไปแล้วว่าดีกว่าการเขียนโค้ดแบบเดิมอย่างไร











