1 คะแนน โดย GN⁺ 4 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ใน ASCII ตัว Z ถูกจัดวางไว้ที่ 90 และ a อยู่ที่ 97 โดยมีอักขระ 6 ตัวคั่นกลาง ทำให้ผลต่างของรหัสระหว่างตัวพิมพ์ใหญ่กับตัวพิมพ์เล็กลงตัวที่ 32
  • 32 คือ 2^5 ดังนั้นตัวอักษรพิมพ์ใหญ่และพิมพ์เล็กที่เป็นคู่กัน เช่น A 65 และ a 97 จึงต่างกันเพียงบิตเดียวคือ 00100000
  • การจัดวางแบบนี้ทำให้สามารถแปลงเป็นตัวพิมพ์ใหญ่ได้ด้วยการทำ AND กับค่ากลับบิตของ 32, แปลงเป็นตัวพิมพ์เล็กได้ด้วยการทำ OR กับ 32, และสลับตัวพิมพ์เล็ก-ใหญ่ได้ด้วยการทำ XOR กับ 32
  • ลำดับของตัวอักษรสามารถหาได้โดยทำ AND ระหว่างรหัสอักขระกับ 31 เพื่อคงไว้เฉพาะ 5 บิตล่าง โดย A/a จะได้ 1 และ Z/z จะได้ 26
  • ASCII เป็นชุดเข้ารหัสอักขระยุคแรกที่ใช้ 7 บิต จึงแทนได้เพียง 128 code points และปัจจุบัน 128 code points แรกของ Unicode ก็ยังเหมือนกับ ASCII

การจัดวางของ ASCII และผลต่าง 32

  • ในตาราง ASCII ค่ารหัสของตัวพิมพ์ใหญ่ Z คือ 90 ส่วนตัวพิมพ์เล็ก a ไม่ได้อยู่ค่าถัดไปทันที แต่ถูกวางไว้ที่ 97
  • ระหว่างนั้นมีอักขระ 6 ตัวคือ [ \ ] ^ _ `
  • ตัวอักษรภาษาอังกฤษมี 26 ตัว และเมื่อรวม 6 อักขระนี้เข้าไปจะได้ 26 + 6 = 32
  • 32 คือค่า 2^5 จึงทำให้ความสัมพันธ์ระหว่างตัวพิมพ์ใหญ่กับตัวพิมพ์เล็กถูกจัดเรียงให้ต่างกันเพียงบิตเฉพาะตำแหน่งเดียว
  • ตัวอย่างเช่น A คือ 65 หรือ 01000001 และ a คือ 97 หรือ 01100001 ซึ่งต่างกัน 32

ความสัมพันธ์ระหว่าง ASCII และ Unicode

  • ASCII เป็นหนึ่งในรูปแบบการเข้ารหัสอักขระยุคแรก โดยใช้เพียง 7 บิต จึงแทน 2^7 = 128 code points ได้
  • code points จำนวน 128 ตำแหน่งนั้นไม่เพียงพอสำหรับเก็บอักขระทั้งหมดที่มนุษย์ใช้ โดยเฉพาะภาษาอย่างภาษาจีนที่มีตัวอักษรนับหมื่นตัว
  • ปัจจุบันชุดอักขระมาตรฐานที่ใช้คือ Unicode ซึ่งมีหลายรูปแบบการเข้ารหัส เช่น UTF-8 และ UTF-16
  • 128 code points แรกของ Unicode เหมือนกับ ASCII ทุกประการ

บิตที่ 5 ซึ่งใช้แยกตัวพิมพ์เล็กและตัวพิมพ์ใหญ่

  • เมื่อนำตัวพิมพ์ใหญ่และตัวพิมพ์เล็กที่เป็นคู่กันมาเทียบในรูปเลขฐานสอง จะพบว่ามีเพียงบิตที่ตรงกับค่า 32 เท่านั้นที่เปลี่ยนไป
65  = 01000001 = A
97  = 01100001 = a

66  = 01000010 = B
98  = 01100010 = b

67  = 01000011 = C
99  = 01100011 = c
  • 32 ในเลขฐานสองคือ 00100000 และบิตนี้เองที่สร้างความต่างระหว่างตัวพิมพ์ใหญ่กับตัวพิมพ์เล็ก
  • การจัดวางตัวอักษรใน ASCII จึงทำให้สามารถแปลงตัวพิมพ์เล็ก-ใหญ่ได้ง่ายด้วยการทำ bitwise operation

จัดการตัวพิมพ์เล็ก-ใหญ่ด้วย bitwise operation

  • แปลงเป็นตัวพิมพ์ใหญ่

    • หากต้องการทำให้อักขระเป็นตัวพิมพ์ใหญ่ ให้ทำ bitwise AND กับค่ากลับบิตของ 32
0 1 1 0 0 0 0 1 (97 = 'a')
& 1 1 0 1 1 1 1 1 (mask)
-------------------
0 1 0 0 0 0 0 1 (65 = 'A')
  • เมื่อนำไปใช้กับ a ค่า 97 จะเปลี่ยนเป็น 65 กลายเป็น A
  • ถ้านำการดำเนินการเดียวกันไปใช้กับ A ที่เป็นตัวพิมพ์ใหญ่อยู่แล้ว ก็จะยังคงเป็น A เหมือนเดิม
  • แปลงเป็นตัวพิมพ์เล็ก

    • หากต้องการทำให้อักขระเป็นตัวพิมพ์เล็ก ให้ทำ bitwise OR กับ 32
0 1 0 0 0 0 0 1 (65 = 'A')
| 0 0 1 0 0 0 0 0 (32)
-------------------
0 1 1 0 0 0 0 1 (97 = 'a')
  • เมื่อนำไปใช้กับ A ค่า 65 จะเปลี่ยนเป็น 97 กลายเป็น a
  • ถ้านำการดำเนินการเดียวกันไปใช้กับ a ที่เป็นตัวพิมพ์เล็กอยู่แล้ว ก็จะยังคงเป็น a เหมือนเดิม
  • สลับตัวพิมพ์เล็ก-ใหญ่

    • หากต้องการสลับตัวพิมพ์เล็ก-ใหญ่ ให้ทำ bitwise XOR กับ 32
0 1 1 0 0 0 0 1 (97 = 'a')
^ 0 0 1 0 0 0 0 0 (32)
-------------------
0 1 0 0 0 0 0 1 (65 = 'A')

0 1 0 0 0 0 0 1 (65 = 'A')
^ 0 0 1 0 0 0 0 0 (32)
-------------------
0 1 1 0 0 0 0 1 (97 = 'a')
  • a จะเปลี่ยนเป็น A และ A จะเปลี่ยนเป็น a

หาลำดับตัวอักษรจาก 5 บิตล่าง

  • ลำดับของตัวอักษรสามารถหาได้โดยทำ bitwise AND ระหว่างรหัสอักขระกับ 31
0 1 0 0 0 0 0 1 (65 = 'A')
& 0 0 0 1 1 1 1 1 (31)
-------------------
0 0 0 0 0 0 0 1 (1)

0 1 1 1 1 0 1 0 (122 = 'z')
& 0 0 0 1 1 1 1 1 (31)
-------------------
0 0 0 1 1 0 1 0 (26)
  • 31 ในเลขฐานสองคือ 00011111 จึงลบบิตด้านหน้าออกและคงไว้เฉพาะ 5 บิตล่าง
  • ใน ASCII ค่า 5 บิตล่างของตัวอักษรจะตรงกับตำแหน่งของตัวอักษรในลำดับพอดี
  • A/a ลงท้ายด้วย 00001 จึงได้ 1, B/b ลงท้ายด้วย 00010 จึงได้ 2, และ Z/z ลงท้ายด้วย 11010 จึงได้ 26
  • ในรหัสอักขระ ASCII ค่า c & 31 จะเท่ากับ c % 32
  • เพราะ 32 เป็นกำลังของ 2 การ mask ด้วย 31 จึงเท่ากับการตัดกลุ่มทีละ 32 ออกไปและเก็บเฉพาะส่วนที่เหลือไว้
'A' = 65  → 65 % 32 = 1
'B' = 66  → 66 % 32 = 2
...
'Z' = 90  → 90 % 32 = 26
'a' = 97  → 97 % 32 = 1
'b' = 98  → 98 % 32 = 2
...
'z' = 122 → 122 % 32 = 26

1 ความคิดเห็น

 
GN⁺ 4 시간 전
ความคิดเห็นจาก Lobste.rs
  • คำอธิบายก็โอเค แต่รู้สึกว่าฝั่ง https://garbagecollected.org/2017/01/31/four-column-ascii/ อธิบายได้ดีกว่า
    ไม่ใช่แค่เรื่องของ Shift อย่างเดียว แต่ Ctrl ก็เกี่ยวด้วย เช่น Tab คือ Ctrl-I โดยที่ I คือ 1001001 และ Ctrl จะ mask บิตแรกออก เหลือเป็น Tab 0001001

  • ถ้าใช้ ตรรกะแบบเครื่องกลไฟฟ้า แทนตรรกะอิเล็กทรอนิกส์เหมือนผู้ผลิตในยุค 1960s bit paired keyboard จะทำได้ง่ายกว่ามาก
    ปุ่ม Shift แค่ toggle บิตเดียวในอักขระ ASCII ก็พอ ทุกวันนี้ใส่ CPU เอนกประสงค์ลงในคีย์บอร์ดทุกตัวและตรรกะแทบจะฟรีอยู่แล้ว แต่ในยุคที่คอมพิวเตอร์เอนกประสงค์มีขนาดเต็มห้อง วิธีแก้แบบนั้นแพงกว่ามาก

  • บทความเสนอว่าแรงจูงใจของการจัดวาง ASCII คือทำให้การแปลงตัวพิมพ์เล็ก/ใหญ่มีประสิทธิภาพด้วย การดำเนินการระดับบิต แต่คุณค่าของมัน ต่อให้ในเชิงประวัติศาสตร์จะสำคัญ ก็ยังดูบางมากในยุคปัจจุบัน
    การแปลง a→A ใช้ได้แค่กับข้อความธรรมดาเท่านั้น และแม้ ISO-8859-1 หรือ Unicode จะขยายการจัดวางนี้ไปบางส่วนกับอักขระอย่าง ü, ç การแปลงตัวพิมพ์เล็ก/ใหญ่ก็ยังขึ้นกับภูมิภาค บริบท และช่วงเวลา การจับคู่ตัวพิมพ์ใหญ่/เล็กที่ถูกต้องของ ß หรือ ï ขึ้นอยู่กับภาษา หน่วยงานกำกับดูแล พื้นที่ใช้งาน และยุคสมัย
    Unicode มีตาราง mapping และ folding สำหรับตัวพิมพ์เล็ก/ใหญ่ เช่น https://www.unicode.org/charts/case/ ซึ่งให้คำตอบที่ใกล้เคียงได้ในกรณีทั่วไป แต่เพราะเป็นปัญหาที่มีนโยบายของมนุษย์เข้ามาเกี่ยว ความซับซ้อนจึงแทบไร้ขีดจำกัด และถ้าจะให้ถูกจริงอาจต้องใช้ซอฟต์แวร์ที่ปรับเฉพาะทาง
    ทุกวันนี้ ถ้าไม่สามารถรับประกันได้ว่าข้อมูลจะถูกจำกัดอยู่แค่ 2⁷ code point แรกของ Unicode ค่าออฟเซ็ต 0b0010_0000 ก็พังได้ง่าย และย่อมต้องมี code path ที่แพงกว่าการดำเนินการบิตเดียวเข้ามาอย่างหลีกเลี่ยงไม่ได้ แม้ใน CPython เอง ''.upper จะผ่าน ASCII fast path ของ unicode_upper{,_impl} แต่ในความเป็นจริงก็ยังต้องผ่าน branch, inline function, การเข้าถึง struct, ฟังก์ชัน และ ฟังก์ชัน หลายตัว, macro และทำ table lookup
    การติดตั้ง ''.upper ในภาษาสมัยใหม่ก็น่าจะมีความซับซ้อนระดับใกล้เคียงกัน และต่อให้คอมไพเลอร์ช่วย optimize แนวทางสมัยใหม่ก็ดูจะเลี่ยงไม่ได้ที่จะมีค่าใช้จ่ายสูงกว่าการดำเนินการระดับบิตเพียงครั้งเดียว ข้อดีของการออกแบบนี้น่าจะเด่นชัดเฉพาะในซอฟต์แวร์เฉพาะทาง เช่น การทำ arithmetic กับ numpy.ndarray ที่เป็น dtype=uint8 ซึ่งเก็บข้อมูลไบนารีดิบหรือข้อมูล Unicode ที่มี code point ไม่เกิน 2⁷
    สิ่งที่สงสัยคือ: ถ้าสมมติว่าแรงจูงใจเดียวของการเลือกออกแบบนี้คืออย่างที่บทความบอก ตอนนั้นก็ยังมีทางเลือกอย่าง ตาราง lookup ขนาด 128 ไบต์ อยู่แล้ว แล้วในประวัติศาสตร์คอมพิวเตอร์ มีช่วงไหนบ้างที่การดำเนินการบิตเดียวมีประสิทธิภาพหรือทำได้จริงมากกว่าการ lookup ตาราง 128 ไบต์?

    • เวลาไปขุดดูเทคโนโลยีมวลชนที่ประสบความสำเร็จมายาวนาน มักจะเจอ bit trick แบบนี้บ่อย
      การตัดสินใจออกแบบในช่วงเวลาหนึ่ง ไปยึดรูปร่างของบางอย่างไว้บนสมมติฐานที่ภายหลังจะใช้ไม่ได้ และทำให้การขยายต่อยากหรือแทบเป็นไปไม่ได้ บทความเรื่อง IPv6 ช่วงหลัง ๆ ก็สะท้อนให้เห็นว่าการเลือกของ IPv4 และ IPv6 ที่เคยถูกต้องในตอนนั้น กลายเป็นภาระมหาศาลในวันนี้
      ถ้ามองในแง่ดี การเลือกแบบนี้คงเกิดจากการชั่งน้ำหนักระหว่างความเสี่ยงกับผลประโยชน์ ประมาณว่า “ถ้าเปลี่ยนอักขระหนึ่งตัวเป็นตัวพิมพ์ใหญ่ได้ด้วยการดำเนินการบิตเดียว มันก็เร็วกว่าเยอะ จึงคุ้มจะรับความเสี่ยงว่าสักวันหนึ่งพอมีผู้ใช้ภาษาญี่ปุ่นแล้วมันจะพัง”
      จากมุมมองของคนปัจจุบัน การเลือกในปี 1976 ทำให้ชีวิตในปี 2026 ยากขึ้น แต่ถ้าในปี 1976 คุณไม่เคยมีคอมพิวเตอร์ใช้ คุณก็ไม่ได้รับประโยชน์นั้นเลยและเหลือแต่ข้อเสีย เมื่อพยายามวางความเห็นแก่ตัวแบบนี้ลง ผมก็เลยสงสัยว่า ถ้าจะสร้างซอฟต์แวร์ที่ยังได้รับความนิยมอีก 20 ปีข้างหน้า เราจะคาดการณ์ ความยั่งยืน ของการเลือกแบบนี้ให้ดีขึ้นได้อย่างไร
    • มี network protocol จำนวนมากที่ใช้อินโค้ดดิ้งแบบ ไม่สนตัวพิมพ์เล็ก/ใหญ่เฉพาะ ASCII ดังนั้น tolower แบบเร็วระดับบิตก็ยังมีประโยชน์อยู่ https://dotat.at/@/2022-06-27-tolower-swar.html
  • อย่างน้อยใน ASCII ตัวอักษรก็ยังเรียงต่อเนื่องกัน EBCDIC มีช่วงห่างเป็น 0x40 (64) และเมื่อเทียบกับ ASCII มันเหมือนมีแถวละ 9 ตัวสองแถวกับแถวละ 8 ตัวอีกหนึ่งแถววางซ้อนขึ้นลง
    https://en.wikipedia.org/wiki/EBCDIC

    • แม้แต่ในบรรดา character code 6 บิตก่อน EBCDIC ก็ยังมีอะไรแปลก ๆ อยู่ไม่น้อย https://en.wikipedia.org/wiki/Six-bit_character_code/…
      ส่วนรหัส 5 บิต http://www.quadibloc.com/crypto/images/tele38.gif และอุปกรณ์ต่อพ่วง CDC บางตัวที่ถ้าจำไม่ผิดใช้รหัส 6 บิตซึ่งยัด control character สำหรับเปลี่ยนหน้าไว้เต็มไปหมดนั้น ขอละไว้ไม่พูดดีกว่า
  • ทำให้นึกถึงตอนที่ชื่อเล่น IRC เป็นแบบ ไม่สน ASCII ตัวพิมพ์เล็ก/ใหญ่ เลยทำให้ foo{ เท่ากับ foo[ และ bar| เท่ากับ bar\
    จะไม่แปลกใจเลยถ้ายังมีไคลเอนต์บางตัวสับสนเพราะเรื่องนี้อยู่