Digging low-level : Bitwise Operation ทำงานอย่างไร?

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:

  1. การตรวจสอบเลขคู่/คี่:
    ถ้า (n & 1) == 0 แสดงว่า n เป็นเลขคู่
    ถ้า (n & 1) == 1 แสดงว่า n เป็นเลขคี่

  2. การตรวจสอบสถานะของบิตเฉพาะ:
    ถ้าต้องการตรวจสอบบิตที่ 3 ของ x:
    if (x & (1 << 2)) != 0 // บิตที่ 3 เป็น 1

  3. การลบบิตสุดท้ายที่เป็น 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:

  1. การรวมแฟล็ก:
    เช่น ในการตั้งค่าสิทธิ์ไฟล์ในระบบ Unix
    permissions = READ | WRITE | EXECUTE

  2. การตั้งค่าบิตเฉพาะ:
    ถ้าต้องการตั้งค่าบิตที่ 3 ของ x เป็น 1:
    x = x | (1 << 2)

  3. การรวมสี 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:

  1. การสลับค่าโดยไม่ใช้ตัวแปรเพิ่ม:
    a ^= b;
    b ^= a;
    a ^= b;
    // ตอนนี้ a และ b ได้สลับค่ากันแล้ว

  2. การเข้ารหัสอย่างง่าย:
    encrypted = data ^ key
    decrypted = encrypted ^ key // จะได้ data กลับคืนมา

  3. การตรวจสอบความเท่ากันของตัวเลข:
    if ((a ^ b) == 0) // a และ b เท่ากัน

  4. การหาตัวเลขที่ไม่มีคู่ในชุดข้อมูล:
    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:

  1. การสร้าง mask สำหรับการลบบิต:
    clear_mask = ~(1 << n) // สร้าง mask ที่มีบิตที่ n เป็น 0 และที่เหลือเป็น 1
    x &= clear_mask // ลบบิตที่ n ของ x

  2. การหาค่าสัมบูรณ์ของเลขติดลบในระบบ two's complement:
    abs_value = (x ^ (x >> 31)) - (x >> 31)
    // x >> 31 จะได้ 0 สำหรับเลขบวก และ -1 สำหรับเลขลบ

  3. การสลับสถานะของทุกบิต:
    inverted = ~original

  4. การหาค่าสูงสุดของตัวแปรที่มีขนาด n บิต:
    max_value = ~0 >> (32 - n) // สำหรับตัวแปร 32 บิต

BitNot มักใช้ร่วมกับ BitAnd เพื่อลบบิตที่ต้องการ หรือใช้ในการสร้าง mask สำหรับการจัดการบิต

Left Shift (<<):

Left Shift มักใช้สัญลักษณ์ << เป็นการดำเนินการที่เลื่อนบิตทั้งหมดของตัวเลขไปทางซ้ายตามจำนวนที่กำหนด โดยเติม 0 ทางด้านขวา
Left Shift โดยทั่วไปแล้วไม่แตกต่างกันระหว่างเลข unsigned int และ signed int แต่มีบางประเด็นที่ควรระวัง:

  1. ผลลัพธ์:

    • ในกรณีของเลขไม่มีเครื่องหมาย ผลลัพธ์จะเป็นบวกเสมอ
    • สำหรับเลขมีเครื่องหมาย ผลลัพธ์อาจเปลี่ยนเครื่องหมายได้หากบิตเครื่องหมาย (sign bit) ถูกเปลี่ยน
  2. Overflow:

    • ทั้งสองกรณีอาจเกิด overflow ได้ถ้าเลื่อนมากเกินไป
    • สำหรับเลข signed int overflow อาจทำให้เกิดการเปลี่ยนเครื่องหมายโดยไม่คาดคิด
  3. 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:

  1. การคูณด้วยกำลังของ 2:
    x << n เท่ากับ x * (2^n)
    เช่น 5 << 2 = 5 * (2^2) = 5 * 4 = 20

  2. การสร้างหน้ากาก (mask) สำหรับการตั้งค่าบิต:
    mask = 1 << n // สร้างหน้ากากที่มีบิตที่ n เป็น 1 และที่เหลือเป็น 0
    x |= mask // ตั้งค่าบิตที่ n ของ x เป็น 1

  3. การเข้ารหัสข้อมูลหลายบิตในตัวแปรเดียว:
    encoded = (r << 16) | (g << 8) | b // เก็บค่า RGB ในตัวแปร 32 บิต

  4. การทำงานกับบิตแฟล็ก:
    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:

  1. การหารด้วยกำลังของ 2:
    x >> n เท่ากับ x / (2^n) (ปัดเศษลง)
    เช่น 20 >> 2 = 20 / (2^2) = 20 / 4 = 5

  2. การแยกส่วนของข้อมูลที่ถูกเก็บรวมกัน:
    r = (rgb >> 16) & 0xFF // แยกค่าสีแดงจาก RGB 24 บิต
    g = (rgb >> 8) & 0xFF // แยกค่าสีเขียว
    b = rgb & 0xFF // แยกค่าสีน้ำเงิน

  3. การตรวจสอบเครื่องหมายของตัวเลข:
    sign = x >> 31 // สำหรับตัวแปร 32 บิต, จะได้ 0 สำหรับบวก และ -1 สำหรับลบ

  4. การทำ 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