Digging low-level : Bitwise Operation ทำงานอย่างไร?
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