Unsafe

    เขียนตามคำขออีกแล้วครับ Unsafe เป็นความสามารถหนึ่ง ที่ลงลึกถึงระดับต่ำ ถ้าคนที่เคยใช้ภาษา C หรือ C++ คงจะรู้จักเป็นอย่างดี นั่นก็คือ Pointer นั่นเอง ก็อย่างที่ผมเคยเขียนในเรื่องของ การเลือกภาษา ว่า Pointer นี้มีคุณอนันต์ แต่ถ้าใช้ไม่ถูกหลัก มันก็จะมีโทษมากเช่นกัน ภาษาระดับสูง อย่าง Visual Basic และ Java ก็ได้ตัดเอา Pointer ทิ้งทั้งหมด เหลือแต่สิ่งที่เรียกว่า Reference แต่ภาษา C# นำเอา Pointer แบบภาษา C กลับมาใช้อีก ผมยังไม่มีข้อมูลพอที่จะสรุปได้ว่า แนวคิดการนำกลับเอามาใช้นี้ จะรุ่งหรือจะริ่ง

    แต่ถึงคุณจะรู้จัก Pointer จากภาษา C หรือ C++ แต่มันก็ยังไม่เพียงพอสำหรับ C# เพราะ C / C++ นั้นการจองหน่วยความจำ จะเรียกจาก OS โดยตรง ซึ่งตำแหน่งที่ Pointer ชี้นั้นจะชี้ไปที่เดิมตลอดเวลาจนกว่าจะเราจะยกเลิกการใช้งานหน่วยความจำส่วนนั้น แต่สำหรับ C# นั้นไม่ใช่ครับ เมื่อเราขอจองหน่วยความจำ ภาษา C# ไม่ได้ขอจองโดยตรงกับ OS แต่หากเราต้องขอจองผ่าน CLR ซึ่งทำหน้าที่เป็นนายหน้า ไปจองหน่วยความจำจาก OS อีกที ซึ่งเหตุผลหลักในการจองผ่าน CLR นั้นก็คือ CLR มีความสามารถในการทำ Garbage Collector เพื่อแก้ปัญหา Memory Leak  และ Garbage Collector นั้นทำให้เราประหยัดหน่วยความจำได้ โดยการทำ Defrag Memory ยกตัวอย่างเช่นถ้ามีคุณจองหน่วยความจำเพียง 10 bytes แล้วเลิกใช้ กว่า Memory นั้นจะนำกลับมาใช้ได้อีก ก็ต้องมีการขอจองหน่วยความจำขนาด 10 bytes หรือเล็กกว่าเท่านั้น มันทำให้เกิด fragment ในหน่วยความจำ ตัว Garbage Collection จะมีการทำ Defrag หน่วยความจำได้ ซึ่งส่งผลให้ ตัวชี้ที่เคยชี้อยู่ที่หน่วยความจำใดๆ จะมีตำแหน่งไม่แน่นอน เปลี่ยนแปลงไปเรื่อยๆ นั่นเป็นเหตุผลว่าทำไมภาษา อย่าง Java จึงไม่มี Pointer มีแต่ Reference (Reference ก็คือ Pointer ที่คุณจะไม่มีสิทธิขอดู Address มันให้ดูไม่ได้ครับเพราะเปลี่ยนไปเรื่อยๆ)

    คำว่า Garbage Collector หรือคนเก็บขยะนั้น ชื่อมันค่อนข้างสื่อครับ เพราะ เราถือว่าหน่วยความจำที่เราเคยจอง แต่ยกเลิกการใช้แล้ว เราถือว่าเป็นขยะครับ ดังนั้น .NET Framework จึงสร้างตัวเก็บขยะมาเพื่อกวาดพวกขยะเหล่านี้ทิ้งไป

    ผมคงไม่แจงรายละเอียดเกี่ยวกับ Pointer นะครับ เพราะเนื้อหาเกี่ยวกับ Pointer คุณสามารถศึกษาได้เองจากหนังสือภาษา C หรือ C++ ทั่วไปได้อยู่แล้ว

แล้ว C# มี Pointer ได้อย่างไร?

    .NET มีวิธีที่ชาญฉลาดในการรองรับ Pointer นั่นก็คือ ถ้าเราต้องการใช้ Pointer กับหน่วยความจำส่วนไหน เราต้องบอก Garbage Collector ไม่ให้ทำการเคลื่อนย้ายตำแหน่งของหน่วยความจำนั้น ซึ่งการทำเช่นนี้เราเรียกว่า Pinning (ตอกหมุด) ซึ่ง keyword ที่ C# ใช้ในการทำ Pinning นั่นก็คือ fixed นั่นเอง แต่คงต้องบอกกันก่อนว่า เราไม่ควรจะ pin หน่วยความจำส่วนใดก็ตามเป็นระยะเวลานาน เพราะมันจะทำให้ Garbage Collector ทำงานไม่เต็มประสิทธิภาพ

   

Microsoft คิดอย่างไรถึงเอา Pointer กลับมาใช้?

    ในมุมมองของ Microsoft การใช้ Pointer นั้นจะทำให้โปรแกรมทำงานได้เร็วขึ้น (ผมทดสอบดูแล้ว มันเร็วขึ้นไม่มากเลย ถ้ามองในแง่นี้ผมคงไม่เห็นด้วยครับ) แต่อีกมุมมองอีกมุมของ Microsoft ก็น่าฟังครับ เขาบอกว่า การที่ทำให้ C# นั้นสามารถเข้ากันได้กับ COM เดิม บางทีก็ไม่สามารถหลีกเลี่ยงการใช้งาน Pointer ได้ แบบนี้ก็พอฟังได้ครับ

ทำไมถึง Unsafe?

    เรื่องนี้เข้าใจไม่ยากครับ ก็ Microsoft ตั้งใจ สร้าง .NET Framework ให้เป็น Platform ที่มีความ Stable สูง เมื่อเวลาที่เรารันโปรแกรม ตัว CLR จำเป็นต้องคอมไพล์ให้เป็น Native Code ก่อนค่อยรัน แต่ก่อนคอมไพล์โปรแกรมนั้น มันจะผ่าน Module ตัวหนึ่งที่ชื่อว่า Verifier เพื่อทำการตรวจสอบดูให้แน่ใจว่า Code นั้นจะไม่รันนอกลู่นอกทางจนรวนระบบ เราจึงเรียกโปรแกรมบน .NET Framework ว่าเป็น Safe Program

    แต่ความสามารถที่เราจะใช้เรา Pointer ได้ มันเป็นดาบสองคม, Pointer นั้นมีความยืดหยุ่นมากเกินไป จนสามารถไปชี้ที่ใดๆ ก็ได้ ซึ่งอาจจะรวนระบบได้ Microsoft จึงมีการป้องกันไม่ให้เราใช้ Pointer ครับ ยกเว้นเราต้องการใช้งานมันจริงๆ เราต้องปลดล็อก 2 ชั้นครับ ซึ่ง Microsoft ใช้ Keyword unsafe ครับเป็นตัวปลดล็อกตัวแรก ซึ่งผมว่าเป็นการเลือก keyword ที่เหมาะสมครับ เพราะเป็นการเตือนโปรแกรมเมอร์ ไปในตัวครับ ส่วนล็อกตัวที่สองที่ต้องปลดนั้น ก็คือเวลาคอมไพล์ครับ ต้องระบุ option ให้ยอมรับ pointer ครับ

    ในเรื่องการปลดล็อกตัวแรก ที่เราใช้ keyword unsafe นั้น เราสามารถ กำหนดบล็อกของ unsafe คร่อม code ที่มี pointer หรือใช้ในระดับ Method หรือ class ก็ได้ ลองดูตัวอย่างข้างล่างครับ

ตัวอย่าง การแปลง string เป็นตัวอักษรตัวใหญ่

     โดยปกติแล้ว string ในภาษา C# แก้ไขไม่ได้ เมื่อมีการเปลี่ยนแปลงใดใน string ภาษา C# จะแอบไปสร้าง string ตัวใหม่ ซึ่งเรื่องนี้ก็จำเป็นครับ เพราะเราจองเนื้อที่มาพอดีกับ string ปัจจุบัน ถ้ามีการขยาย string นั้น เราต้องลอกไปอีกที่หนึ่ง แต่ บางกรณี ถ้าเราต้องการแปลง ตัวอักษรตัวเล็กเป็นตัวอักษรตัวใหญ่ ซึ่งที่จริงแล้วไม่มีการเปลี่ยนขนาดของ string แต่ภาษา C# ยังไงก็ต้องสร้าง string ใหม่อยู่ดี

    แต่ด้วย Pointer เราสามารถทำได้ครับ ลองดู code ข้างล่าง

ToUpper.cs

class Hello
{
	static unsafe void ToUpper(string str) 
	{
		fixed(char *str2 = str) {
			char *ptr = str2;
			while (*ptr != '\0') {
				if (*ptr >= 'a' && *ptr <= 'z') {
					*ptr = (char)((byte)*ptr - (byte)'a' + (byte)'A');
				}
				ptr++;
			}
		}
	}
	static void Main()
	{
		string str = "Hello, World";
		ToUpper(str);
		Console.WriteLine(str);
		Console.ReadLine();
	}
}

  

    เริ่มต้น ผมให้ unsafe ในระดับ method ผมว่า น่าจะเป็นจุดที่นิยมกันมากที่สุด จากนั้น เพื่อบังคับให้ str ที่ส่งมาเป็นพามามิเตอร์ อยู่นิ่งๆ ไม่ให้ Garbage Collector ย้ายตำแหน่ง เราจะได้เอา pointer เข้าไปวิ่งได้ เราต้องทำการ pinning ครับ โดยใช้ keyword fixed ถ้าคุณต้องการ pin หลายตัวแปร คุณก็สามารถใช้เครื่องหมาย , คั่น และเราต้องมี pointer เพื่อชี้ตัวแปรแต่ละตัวด้วยครับ

    แต่ pointer ที่เราสร้างมาในวงเล็บของ fixed() นั้นวิ่งไม่ได้นะครับ เรื่องนี้ผมงงอยู่นานเลยกว่าจะเข้าใจ เราจำเป็นต้อง copy ไปยัง pointer ตัวอื่นครับ ตามตัวอย่างนี้ ผม copy จาก str2 ไปยัง ptr จากนั้นก็วิ่งได้แล้วล่ะครับ

    เวลาคอมไพล์คุณต้อง คอมไพล์ด้วย csc /unsafe toupper.cs

    แต่ถ้าใครใช้ Visual Studio.NET ก็ให้ไปที่ Solution Provider กดขวาเพื่อเลือก Property ลองหา Allow unsafe code blocks ใน configuration properties  ให้ set เป็น true ครับ

stackalloc

โดยปกติแล้ว เมื่อเราใช้คำสั่ง new ในภาษา C# มันจะไปจองเนื้อที่ ให้เราบน heap ซึ่ง heap นี่เอง อยู่ในความควบคุมของ Garbage Collector ซึ่งมันอาจจะเสียประสิทธิภาพพอสมควร ดังนั้น ภาษา C# จึงยอมให้เราจองเนื้อที่จาก stack ซึ่ง stack ดังกล่าวก็คือหน่วยความจำที่เก็บตัวแปรที่เป็น local นั่นเอง ซึ่งหน่วยความจำดังกล่าวจะถูกทิ้งไป เมื่อจบ method ครับ เราสามารถใช้ stackalloc() กับ pointer เท่านั้นครับ ลองดูตัวอย่าง

 

bubble.cs

public class Bubble
{
	const int N = 100000;
	public unsafe static void unsafeSort() {
		Random r = new Random(1);
        	int* arr =  stackalloc int[N];
		int i, j, temp;

		Console.WriteLine(DateTime.Now);

		for (i=0; i<N; i++) {
			arr[i] = r.Next();
		}

		for (i=1; i<N; i++) {
			for (j=i; j>0; j--) {
				if (arr[j-1] > arr[j]) {
					temp = arr[j-1];
					arr[j-1] = arr[j];
					arr[j] = temp;
				}
			}
		}
		Console.WriteLine(DateTime.Now);	
	}
	public static void safeSort() {
		Random r = new Random(1);
		int[] arr = new int[N];
		int i, j, temp;

		Console.WriteLine(DateTime.Now);
		for (i=0; i<N; i++) {
			arr[i] = r.Next();
		}
		for (i=1; i<N; i++) {
			for (j=i; j>0; j--) {
				if (arr[j-1] > arr[j]) {
					temp = arr[j-1];
					arr[j-1] = arr[j];
					arr[j] = temp;
				}
			}
		}
		Console.WriteLine(DateTime.Now);
	}

	public static void Main() {
		Console.WriteLine("Safe Sort\n=========");
		safeSort();
		Console.WriteLine("\nUnSafe Sort\n===========");
		unsafeSort();
	}
}

 เมื่อรันแล้วจะได้ผลลัพธ์ดังนี้ครับ

DOS Prompt

C:\cs> bubble

Safe Sort

=========
10/17/2001 9:28:50
10/17/2001 9:30:28

UnSafe Sort
===========
10/17/2001 9:30:28
10/17/2001 9:31:33


C:\cs> _

    โปรแกรมผมจับเวลาการเรียงข้อมูล 2 แบบ แบบแรกคือจองบน Heap และแบบที่ 2 คือจองบน stack แบบบน Heap นั้นคือ Array ธรรมดา ส่วนแบบที่ 2 เป็น Pointer ครับ ผมเลือกใช้ Algorithm Bubble Sort ซึ่งทำงานได้ช้า เมื่อข้อมูลขนาดใหญ่ ก็ตั้งใจให้เห็นเวลากันชัดๆ ครับ ถ้าเราใช้ Array ธรรมดา มันเป็น Reference Type ดังนั้นมันต้องชี้เข้าหา Array ก่อน แล้ว Array ค่อยชี้หาตำแหน่ง ซึ่ง เป็น pointer 2 ต่อ ส่วนถ้าเราเอาข้อมูลโดยตรงจาก stack มันจะเป็น pointer ชั้นเดียว ซึ่งว่าตามทฤษฏีแล้ว มันต้องเร็วกว่าครับ แต่เร็วกว่าแค่ไหน ลองรันดูครับ

    ผลการรันบนเครื่องของผม (Atlon 1333) ก็ได้ผลลัพธ์อย่างที่เห็นครับ ถ้าใช้ array จะใช้เวลา 1:38 นาที ส่วนถ้าใช้ stackalloc ก็ใช้ 1:05 นาที ถึงแม้ว่าดูแล้วเวลาจะต่างกันกว่า 40% แต่ผมว่างานจริงๆ ที่ใช้ array ใหญ่ขนาดนี้คงไม่พบบ่อยๆ ครับ ถ้าข้อมูลน้อยๆ คงไม่ค่อยรู้สึกถึงความแตกต่างเท่าไหร่ครับ

    เรื่องนี้ทำให้ผมนึกถึงเมื่อกว่า 15 ปีที่แล้ว ตอนที่หัดเรียน Algorithm ใหม่ๆ บนเครื่อง Apple ][ ครับ ในยุคนั้น Array ขนาด 4,000 ตัว ถ้าใช้ Bubble Sort จากเช้าถึงเย็นก็ไม่เสร็จครับ แต่วันนี้ ข้อมูล 100,000 ตัว ใช้เวลานาทีเดียว มันเร็วขึ้นมากครับ

    ผมว่าผมขาดตัวอย่างที่ใช้เครื่องหมาย & แต่ผมว่าถ้าคุณรู้จักภาษา C หรือ C++ คุณคงใช้เป็นอยู่แล้ว แต่ถ้าคุณไม่รู้จัก pointer ผมว่าคุณหาหนังสือ C / C++ อ่านก่อนครับ เพราะมันมีรายละเอียดเยอะมาก