มีอะไรใหม่ใน .NET Core 3 และ C# 8 : Stackalloc ซ้อนนิพจน์

มีอะไรใหม่ใน .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
ตัวกระทำ 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
และไม่จำเป็นต้องนำผลที่ได้ไปเก็บในตัวแปรแบบ Span<T> หรือ ReadOnlySpan<T>
และเราอาจใช้นิพจน์ stackalloc เพื่อจองหน่วยความจำอย่างมีเงื่อนไขได้
รูปที่ 2 แสดงตัวอย่างโค้ดการใช้งานนิพจน์ stackalloc
- บรรทัด 9 ประกาศตัวแปรไว้ทำหน้าที่กำหนดขนาดพื้นที่ของหน่วยความจำที่ต้องการจองไว้ใน Stack
- บรรทัด 10 จองหน่วยความจำใน Stack มีขนาดตามตัวแปร length
- บรรทัด 13 สาธิตวิธีนำค่าเข้าไปเก็บในหน่วยความจำใน Stack ที่จองไว้
- บรรทัด 17-20 สาธิตวิธีใช้นิพจน์ stackalloc เพื่อจองหน่วยความจำอย่างมีเงื่อนไข ตัวอย่างที่เห็นนี้คือถ้าพื้นที่มีขนาดเล็กกว่า 1,024 ให้ใช้ตัวกระทำ stackalloc เพื่อจองหน่วยความจำใน Stack
ตัวแปรแบบ 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 โค้ดนี้สาธิตการจองพื้นที่ในหน่วยความจำแบบให้อ่านได้เท่านั้นและกำหนดค่าเริ่มต้นโดยไม่ได้ระบุขนาด แต่พื้นที่จะมีขนาดเท่ากับข้อมูลที่เรากำหนดเป็นค่าเริ่มต้นคือสามหน่วย
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 แสดงผลรวม
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
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 แสดงผลรวม
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 ปล่อยการจองพื้นที่ในหน่วยความจำ
บทความนี้ได้กล่าวถึงคุณสมบัติใหม่ในภาษา C# 8 และ .NET Core 3 ที่เกี่ยวข้องกับ
- stackalloc ซ้อนนิพจน์,
- ตัวกระทำ stackalloc,
- ตัวแปรแบบ pointer กับ stackalloc,
- Span กับ array ,
- Span กับหน่วยความจำที่กำหนด,
- Span กับ Stack และ Span กับหน่วยความจำทั้งสามแบบ