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

เริ่มตั้งแต่ C# 8 และ .NET Core 3.0 ถ้าผลลัพธ์ของนิพจน์ stackalloc มีชนิดข้อมูลเป็นแบบ System.Span<T> หรือ System.ReadOnlySpan<T> เราสามารถใส่นิพจน์ stackalloc ซ้อนไว้ภายในนิพจน์อื่น ๆ ได้
ภาพหน้าปก มีอะไรใหม่ใน .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

รูปที่ 1 

โค้ดตัวอย่างการใช้นิพจน์  stackalloc ซ้อนไว้ภายในนิพจน์อื่น ๆ

ตัวกระทำ 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

โค้ดตัวอย่างการใช้งานนิพจน์  stackalloc

ตัวแปรแบบ 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

โค้ดตัวอย่างการใช้งานนิพจน์  stackalloc กับตัวแปรแบบ pointer

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 กับพื้นที่ใน array

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 กับพื้นที่ในหน่วยความจำที่กำหนด

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 กับพื้นที่ใน Stack

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

โค้ดตัวอย่างการสร้างและใช้งาน Span กับหน่วยความจำทั้งสามแบบ

บทความนี้ได้กล่าวถึงคุณสมบัติใหม่ในภาษา C# 8 และ .NET Core 3 ที่เกี่ยวข้องกับ

  • stackalloc ซ้อนนิพจน์,
  • ตัวกระทำ stackalloc,
  • ตัวแปรแบบ pointer กับ stackalloc,
  • Span กับ array ,
  • Span กับหน่วยความจำที่กำหนด,
  • Span กับ Stack และ Span กับหน่วยความจำทั้งสามแบบ

อย่างไรท่านผู้อ่านทดลองเขียนโค้ดดูเพื่อทดสอบคุณสมบัติใหม่ดังกล่าวไปแล้วว่าดีกว่าการเขียนโค้ดแบบเดิมอย่างไร