Bitwise Operation คือ กระบวนการที่การดำเนินการใด ๆ โดยตรงกับบิตของตัวเลข หรือชุดข้อมูลของเราใน memory ซึ่งการทำงานโดยตรงกับบิตของตัวเลข ทำให้มีประสิทธิภาพสูงและใช้บ่อยในการเขียนโปรแกรมระดับ low-level โดยหลัก ๆ ที่ใช้กันจะมี 6 แบบ เป็นเครื่องมือที่ทรงพลังสำหรับนักพัฒนาที่ต้องการประสิทธิภาพสูงและการควบคุมระดับบิต โดยเฉพาะในการพัฒนาระบบระดับ low-level และการทำงานกับฮาร์ดแวร์โดยตรง
TLDR
ภาพรวม Bitwise Operations:
AND (&):
ใช้เปรียบเทียบบิต, ให้ 1 เมื่อทั้งสองบิตเป็น 1 ประโยชน์: ตรวจสอบบิต, สร้าง mask
OR (|):
ให้ 1 เมื่อมีบิตใดบิตหนึ่งเป็น 1 ประโยชน์: รวมแฟล็ก, ตั้งค่าบิต
XOR (^):
ให้ 1 เมื่อบิตต่างกัน ประโยชน์: สลับค่า, เข้ารหัสอย่างง่าย
NOT (~):
กลับค่าทุกบิต ประโยชน์: สร้าง mask สำหรับลบบิต
Left Shift (<<):
เลื่อนบิตไปซ้าย, เติม 0 ทางขวา ประโยชน์: คูณด้วยกำลังของ 2, สร้าง mask
Right Shift (>>):
เลื่อนบิตไปขวา ประโยชน์: หารด้วยกำลังของ 2, แยกส่วนข้อมูล
1.) BitAnd (&):
BitAnd มักใช้สัญลักษณ์ & เป็นการดำเนินการทางตรรกะที่เปรียบเทียบบิตในตำแหน่งเดียวกันของสองตัวเลข โดยมีกฎดังนี้:
- 1 AND 1 = 1
- 1 AND 0 = 0
- 0 AND 1 = 0
- 0 AND 0 = 0
หรือกล่าวอีกนัยหนึ่ง ผลลัพธ์จะเป็น 1 ก็ต่อเมื่อทั้งสองบิตเป็น 1 เท่านั้น
ตัวอย่าง: สมมติว่าเรามีเลขสองตัว A = 5 และ B = 3 ในระบบเลขฐานสอง: A = 5 = 0101 B = 3 = 0011
เมื่อทำ BitAnd:
0101 (A = 5)
& 0011 (B = 3)
----
0001 (Result = 1)
จะเห็นได้ว่า:
- บิตที่ 1 (จากขวา): 1 & 1 = 1
- บิตที่ 2: 0 & 1 = 0
- บิตที่ 3: 1 & 0 = 0
- บิตที่ 4: 0 & 0 = 0
ดังนั้น ผลลัพธ์คือ 0001 ในเลขฐานสอง ซึ่งเท่ากับ 1 ในเลขฐานสิบ
การใช้งาน BitAnd:
การตรวจสอบเลขคู่/คี่: ถ้า (n & 1) == 0 แสดงว่า n เป็นเลขคู่ ถ้า (n & 1) == 1 แสดงว่า n เป็นเลขคี่
การตรวจสอบสถานะของบิตเฉพาะ: ถ้าต้องการตรวจสอบบิตที่ 3 ของ x: if (x & (1 << 2)) != 0 // บิตที่ 3 เป็น 1
การลบบิตสุดท้ายที่เป็น 1: n = n & (n - 1)
BitAnd มีประโยชน์มากในการจัดการกับข้อมูลระดับบิต การตั้งค่าแฟล็ก และการทำงานกับฮาร์ดแวร์โดยตรง
2.) BitOr (|):
BitOr มักใช้สัญลักษณ์ | เป็นการดำเนินการทางตรรกะที่เปรียบเทียบบิตในตำแหน่งเดียวกันของสองตัวเลข โดยมีกฎดังนี้:
- 1 OR 1 = 1
- 1 OR 0 = 1
- 0 OR 1 = 1
- 0 OR 0 = 0
กล่าวคือ ผลลัพธ์จะเป็น 1 เมื่อมีบิตใดบิตหนึ่งเป็น 1 หรือทั้งสองบิตเป็น 1
ตัวอย่าง: สมมติว่าเรามีเลขสองตัว A = 5 และ B = 3 ในระบบเลขฐานสอง: A = 5 = 0101 B = 3 = 0011
เมื่อทำ BitOr:
0101 (A = 5)
| 0011 (B = 3)
----
0111 (Result = 7)
จะเห็นได้ว่า:
- บิตที่ 1 (จากขวา): 1 | 1 = 1
- บิตที่ 2: 0 | 1 = 1
- บิตที่ 3: 1 | 0 = 1
- บิตที่ 4: 0 | 0 = 0
ดังนั้น ผลลัพธ์คือ 0111 ในเลขฐานสอง ซึ่งเท่ากับ 7 ในเลขฐานสิบ
การใช้งาน BitOr:
การรวมแฟล็ก: เช่น ในการตั้งค่าสิทธิ์ไฟล์ในระบบ Unix permissions = READ | WRITE | EXECUTE
การตั้งค่าบิตเฉพาะ: ถ้าต้องการตั้งค่าบิตที่ 3 ของ x เป็น 1: x = x | (1 << 2)
การรวมสี RGB: ในระบบสี RGB ที่ใช้ 8 บิตต่อสี color = (red << 16) | (green << 8) | blue
BitOr มีประโยชน์มากในการรวมแฟล็ก การตั้งค่าบิต และการทำงานกับข้อมูลที่แต่ละบิตมีความหมายเฉพาะ
BitXor (^):
BitXor หรือ Exclusive OR (XOR) มักใช้สัญลักษณ์ ^ เป็นการดำเนินการทางตรรกะที่เปรียบเทียบบิตในตำแหน่งเดียวกันของสองตัวเลข โดยมีกฎดังนี้:
- 1 XOR 1 = 0
- 1 XOR 0 = 1
- 0 XOR 1 = 1
- 0 XOR 0 = 0
กล่าวคือ ผลลัพธ์จะเป็น 1 เมื่อบิตทั้งสองมีค่าต่างกัน
ตัวอย่าง: สมมติว่าเรามีเลขสองตัว A = 5 และ B = 3 ในระบบเลขฐานสอง: A = 5 = 0101 B = 3 = 0011
เมื่อทำ BitXor:
0101 (A = 5)
^ 0011 (B = 3)
----
0110 (Result = 6)
จะเห็นได้ว่า:
- บิตที่ 1 (จากขวา): 1 ^ 1 = 0
- บิตที่ 2: 0 ^ 1 = 1
- บิตที่ 3: 1 ^ 0 = 1
- บิตที่ 4: 0 ^ 0 = 0
ดังนั้น ผลลัพธ์คือ 0110 ในเลขฐานสอง ซึ่งเท่ากับ 6 ในเลขฐานสิบ
การใช้งาน BitXor:
การสลับค่าโดยไม่ใช้ตัวแปรเพิ่ม: a ^= b; b ^= a; a ^= b; // ตอนนี้ a และ b ได้สลับค่ากันแล้ว
การเข้ารหัสอย่างง่าย: encrypted = data ^ key decrypted = encrypted ^ key // จะได้ data กลับคืนมา
การตรวจสอบความเท่ากันของตัวเลข: if ((a ^ b) == 0) // a และ b เท่ากัน
การหาตัวเลขที่ไม่มีคู่ในชุดข้อมูล: result = num1 ^ num2 ^ num3 ^ … // ผลลัพธ์จะเป็นตัวเลขที่ไม่มีคู่
BitXor มีคุณสมบัติพิเศษคือ การ XOR กับตัวเองจะได้ 0 และการ XOR กับ 0 จะได้ตัวเองกลับคืนมา ทำให้มันมีประโยชน์มากในการเข้ารหัสและการตรวจสอบข้อมูล
BitNot (~):
BitNot หรือ NOT มักใช้สัญลักษณ์ ~ เป็นการดำเนินการทางตรรกะที่กลับบิตของตัวเลข โดยมีกฎง่ายๆ คือ:
- NOT 1 = 0
- NOT 0 = 1
กล่าวคือ BitNot จะเปลี่ยนทุกบิต 0 เป็น 1 และทุกบิต 1 เป็น 0
ตัวอย่าง: สมมติว่าเรามีเลข A = 5 ในระบบเลขฐานสอง (สมมติว่าเป็นเลข 8 บิต): A = 5 = 00000101
เมื่อทำ BitNot:
00000101 (A = 5)
--------
~ 11111010 (Result = 250 ใน unsigned int, หรือ -6 ใน signed int)
จะเห็นได้ว่า:
- ทุกบิต 0 ถูกเปลี่ยนเป็น 1
- ทุกบิต 1 ถูกเปลี่ยนเป็น 0
ผลลัพธ์จะขึ้นอยู่กับว่าเราใช้ระบบเลขแบบไหน (signed หรือ unsigned):
- ในระบบ unsigned 8 บิต: 11111010 = 250
- ในระบบ signed 8 บิต: 11111010 = -6
การใช้งาน BitNot:
การสร้าง mask สำหรับการลบบิต: clear_mask = ~(1 << n) // สร้าง mask ที่มีบิตที่ n เป็น 0 และที่เหลือเป็น 1 x &= clear_mask // ลบบิตที่ n ของ x
การหาค่าสัมบูรณ์ของเลขติดลบในระบบ two’s complement: abs_value = (x ^ (x >> 31)) - (x >> 31) // x >> 31 จะได้ 0 สำหรับเลขบวก และ -1 สำหรับเลขลบ
การสลับสถานะของทุกบิต: inverted = ~original
การหาค่าสูงสุดของตัวแปรที่มีขนาด n บิต: max_value = ~0 >> (32 - n) // สำหรับตัวแปร 32 บิต
BitNot มักใช้ร่วมกับ BitAnd เพื่อลบบิตที่ต้องการ หรือใช้ในการสร้าง mask สำหรับการจัดการบิต
Left Shift (<<):
Left Shift มักใช้สัญลักษณ์ << เป็นการดำเนินการที่เลื่อนบิตทั้งหมดของตัวเลขไปทางซ้ายตามจำนวนที่กำหนด โดยเติม 0 ทางด้านขวา Left Shift โดยทั่วไปแล้วไม่แตกต่างกันระหว่างเลข unsigned int และ signed int แต่มีบางประเด็นที่ควรระวัง:
ผลลัพธ์:
- ในกรณีของเลขไม่มีเครื่องหมาย ผลลัพธ์จะเป็นบวกเสมอ
- สำหรับเลขมีเครื่องหมาย ผลลัพธ์อาจเปลี่ยนเครื่องหมายได้หากบิตเครื่องหมาย (sign bit) ถูกเปลี่ยน
Overflow:
- ทั้งสองกรณีอาจเกิด overflow ได้ถ้าเลื่อนมากเกินไป
- สำหรับเลข signed int overflow อาจทำให้เกิดการเปลี่ยนเครื่องหมายโดยไม่คาดคิด
Undefined Behavior:
- ในบางภาษา เช่น C/C++, การเลื่อนด้วยค่าที่มากกว่าหรือเท่ากับจำนวนบิตของตัวแปรอาจทำให้เกิด Undefined Behavior ได้
ตัวอย่าง: สมมติว่าเรามีเลข A = 5 และต้องการเลื่อนซ้าย 2 ตำแหน่ง ในระบบเลขฐานสอง (สมมติว่าเป็นเลข 8 บิต): A = 5 = 00000101
เมื่อทำ Left Shift 2 ตำแหน่ง (A << 2):
00000101 (A = 5)
--------
00010100 (Result = 20)
จะเห็นได้ว่า::
- บิตทั้งหมดถูกเลื่อนไปทางซ้าย 2 ตำแหน่ง
- เติม 0 สองตัวทางด้านขวา
ผลลัพธ์: 00010100 ในเลขฐานสอง ซึ่งเท่ากับ 20 ในเลขฐานสิบ
ตัวอย่าง 2:
unsigned int a = 1; // 00000001 in binary
int b = 1; // 00000001 in binary
// Left shift by 3
unsigned int a_result = a << 3; // 00001000 (8 in decimal)
int b_result = b << 3; // 00001000 (8 in decimal)
// สำหรับตัวเลขเล็ก ๆ ผลอาจจะไม่ต่างกัน
// แต่เมื่อมีตัวเลขที่ใหญ่ขึ้น อาจจะให้ผลตรงกันข้ามแทน:
unsigned int large_a = 1 << 31; // 10000000 00000000 00000000 00000000 (2147483648 in decimal)
int large_b = 1 << 31; // 10000000 00000000 00000000 00000000 (-2147483648 in decimal, due to two's complement)
ในตัวอย่างสุดท้าย เราเห็นความแตกต่าง:
- สำหรับ unsigned int, ผลลัพธ์คือเลขบวกที่ใหญ่ที่สุดที่เป็นไปได้
- สำหรับ signed int, ผลลัพธ์คือเลขลบที่น้อยที่สุดที่เป็นไปได้ เนื่องจากบิตซ้ายสุดถูกตีความเป็นเครื่องหมายลบในระบบ two’s complement
กฎการทำงาน:
- เลื่อนบิตทั้งหมดไปทางซ้าย n ตำแหน่ง
- เติม 0 ทางด้านขวา n ตัว
- บิตที่เลื่อนออกไปทางซ้ายจะหายไป
การใช้งาน Left Shift:
การคูณด้วยกำลังของ 2: x << n เท่ากับ x * (2^n) เช่น 5 << 2 = 5 * (2^2) = 5 * 4 = 20
การสร้างหน้ากาก (mask) สำหรับการตั้งค่าบิต: mask = 1 << n // สร้างหน้ากากที่มีบิตที่ n เป็น 1 และที่เหลือเป็น 0 x |= mask // ตั้งค่าบิตที่ n ของ x เป็น 1
การเข้ารหัสข้อมูลหลายบิตในตัวแปรเดียว: encoded = (r << 16) | (g << 8) | b // เก็บค่า RGB ในตัวแปร 32 บิต
การทำงานกับบิตแฟล็ก: FLAG_A = 1 << 0 FLAG_B = 1 << 1 FLAG_C = 1 << 2 // ใช้งาน: flags = FLAG_A | FLAG_B
Left Shift มีประสิทธิภาพสูงในการคูณด้วยกำลังของ 2 และมักใช้ในการจัดการบิตแฟล็กและการสร้างหน้ากากสำหรับการดำเนินการระดับบิต
Right Shift (>>):
Right Shift มักใช้สัญลักษณ์ >> เป็นการดำเนินการที่เลื่อนบิตทั้งหมดของตัวเลขไปทางขวาตามจำนวนที่กำหนด การทำงานจะแตกต่างกันเล็กน้อยระหว่างเลขที่มีเครื่องหมาย (signed) และไม่มีเครื่องหมาย (unsigned)
สำหรับเลข unsigned int (Logical Right Shift):
- เลื่อนบิตทั้งหมดไปทางขวา n ตำแหน่ง
- เติม 0 ทางด้านซ้าย n ตัว
- บิตที่เลื่อนออกไปทางขวาจะหายไป
สำหรับเลข signed int (Arithmetic Right Shift):
- เลื่อนบิตทั้งหมดไปทางขวา n ตำแหน่ง
- เติมด้วยบิตเครื่องหมาย (sign bit) ทางด้านซ้าย n ตัว
- บิตที่เลื่อนออกไปทางขวาจะหายไป
ตัวอย่าง: สมมติว่าเรามีเลข A = 20 และต้องการเลื่อนขวา 2 ตำแหน่ง ในระบบเลขฐานสอง (สมมติว่าเป็นเลข 8 บิต): A = 20 = 00010100
เมื่อทำ Right Shift 2 ตำแหน่ง (A >> 2):
00010100 (A = 20)
--------
00000101 (Result = 5)
จะเห็นได้ว่า::
- บิตทั้งหมดถูกเลื่อนไปทางขวา 2 ตำแหน่ง
- เติม 0 สองตัวทางด้านซ้าย (สำหรับเลขไม่มีเครื่องหมาย)
ผลลัพธ์: 00000101 ในเลขฐานสอง ซึ่งเท่ากับ 5 ในเลขฐานสิบ
การใช้งาน Right Shift:
การหารด้วยกำลังของ 2: x >> n เท่ากับ x / (2^n) (ปัดเศษลง) เช่น 20 >> 2 = 20 / (2^2) = 20 / 4 = 5
การแยกส่วนของข้อมูลที่ถูกเก็บรวมกัน: r = (rgb >> 16) & 0xFF // แยกค่าสีแดงจาก RGB 24 บิต g = (rgb >> 8) & 0xFF // แยกค่าสีเขียว b = rgb & 0xFF // แยกค่าสีน้ำเงิน
การตรวจสอบเครื่องหมายของตัวเลข: sign = x >> 31 // สำหรับตัวแปร 32 บิต, จะได้ 0 สำหรับบวก และ -1 สำหรับลบ
การทำ rounded division: (x + (1 << (n-1))) >> n // หารด้วย 2^n และปัดเศษ
ข้อควรระวัง:
- ใน Java, >> เป็น arithmetic shift สำหรับ int และ long, ส่วน >>> เป็น logical shift
- ในภาษาอื่นๆ เช่น C/C++, การทำงานของ >> กับเลขติดลบอาจแตกต่างกันไปตาม Compiler
Right Shift มีประสิทธิภาพสูงในการหารด้วยกำลังของ 2 และมักใช้ในการจัดการข้อมูลที่ถูกเก็บรวมกันในรูปแบบบิต
หากเปรียบกับ Right Shift แม้ว่าการทำงานพื้นฐานของ Left Shift จะเหมือนกันสำหรับทั้งเลข signed int และ unsigned int แต่การตีความผลลัพธ์และผลกระทบต่อเครื่องหมาย(sign bit)อาจแตกต่างกัน ดังนั้นจึงควรระมัดระวังเมื่อใช้ Left Shift กับเลขที่มี signed int โดยเฉพาะเมื่อเลื่อนด้วยค่าที่สูงมาก ๆ
Conclusion
ลักษณะเด่นของ Bitwise Operations:
- ประสิทธิภาพสูง: ทำงานเร็วกว่าการคำนวณปกติ
- ประหยัดหน่วยความจำ: จัดเก็บข้อมูลหลายอย่างในตัวแปรเดียว
- ใช้ในงานระดับต่ำ: การทำงานกับฮาร์ดแวร์, ระบบปฏิบัติการ
- การเข้ารหัส: ใช้ในอัลกอริทึมการเข้ารหัสและบีบอัดข้อมูล
- การจัดการแฟล็ก: ใช้ในการตั้งค่าและตรวจสอบสถานะต่างๆ
การใช้งานทั่วไป:
- จัดการสิทธิ์และการอนุญาตในระบบไฟล์
- การเข้ารหัสและถอดรหัสข้อมูล
- การบีบอัดข้อมูล
- การทำงานกับพิกเซลในการประมวลผลภาพ
- การจัดการโปรโตคอลเครือข่าย
- การทำงานกับ register ใน Embedded system
ข้อควรระวัง:
- ต้องระมัดระวังเรื่องขนาดของข้อมูล (8, 16, 32, 64 บิต)
- ความแตกต่างระหว่างเลขมี signed int และ unsigned int
- ความแตกต่างของการทำงานในแต่ละภาษาโปรแกรม
**unsigned int คือตัวเลขเป็นบวกเสมอ signed int สามารถมีค่าเป็นลบได้
#siamstr #Blog