YOLO เป็นสแลงของวัยรุ่นในแถบประเทศที่พูดภาษาอังกฤษ มันย่อมาจากประโยคที่บอกว่า you only live once ที่แปลว่า "คุณมีชีวิตอยู่แค่ครั้งเดียว" ดังนั้นจะทำอะไรก็ทำไปเถอะ น่าเสียดายว่าหลายคนเอาไปใช้ในทางที่สุดโต่งอย่างการทำผิดกฎหมาย ทำให้คำนี้โดนมองในแง่ลบอย่างหนัก ต่างไปจากวลี carpe diem ในภาษาละตินที่แปลว่า "จงฉกฉวยชั่นขณะนี้ไว้" มันเป็นคำประพันธ์โบราณโดย Horace เมื่อกว่าสองพันปีก่อน และได้รับการเผยแพร่ซ้ำผ่านหนังขึ้นหิ้งอย่าง Dead Poet Society ทำให้วลีนี้ดูสุขุมนุ่มลึกกว่าคำข้างต้นที่เป็นได้เพียงสบถของพวกนอกคอก
แต่บทความนี้ไม่ได้มาเจาะลึกเรื่องทางประวัติศาสตร์ สังคม หรือแม้กระทั่งความหมายของคำนั้นหรอกครับ
เพราะสิ่งที่น่าสนใจยิ่งกว่า คือแนวคิดที่แฝงไว้ว่า "ชีวิตนี้มีแค่ครั้งเดียว"
หลายคนอาจจะรู้สึกว่าแนวคิดเช่นนี้เป็นสิ่งแปลกประหลาด ซึ่งก็เข้าใจได้ เพราะแทบทุกศาสนาบนโลก ต่างพูดเรื่องนี้ในทางตรงกันข้ามว่า "ยังมีเรื่องอื่นๆ อีกหลักความตาย" เช่น แนวคิดเรื่องสวรรค์และนรก แนวคิดการเวียนว่ายตายเกิด
จุดที่ทำให้ศาสนาต่างๆ สร้างความเชื่อเช่นนั้นได้แม้ร่างกายจะสูญสิ้นไป ก็ด้วยแนวคิดที่ว่าร่างกายนั้นประกอบขึ้นมาจากสิ่งของในโลกนี้อย่างกระดูกเลือดเนื้อ และสิ่งของในโลกอื่นอย่างวิญญาณ (soul) ซึ่งหากมันรวมอยู่กับร่างกายแล้วจะช่วยให้มีชีวิต มีความคิด มีความรู้สึก แต่หากแยกออกจากกันก็จะทำให้ร่างกายนั้นตายไป
พูดง่ายๆ ว่าวิญญาณนั้นอยู่ในมิติอื่นที่เราไม่สามารถสังเกตได้นั่นเอง
และแน่นอนว่า เมื่อไหร่ก็ตามที่วิทยาศาสตร์จะพยายามสังเกตวิญญาณให้ได้ ศาสนาก็จะหลบเลี่ยงไปอธิบายวิญญาณในรูปแบบอื่นแทน ดังนั้นบทความนี้จะยังไม่มาเล่นวิ่งไล่จับกัน แต่จะฟันธงไปก่อนเลยว่าวิญญาณนั้นไม่มี เพื่อที่จะได้สำรวจความคิดที่งอกเงยขึ้นมาอย่างเต็มที่
เมื่อเราปฏิเสธเรื่องวิญญาณไปแล้ว จะเห็นได้ว่าส่วนที่เหลือก็ง่ายทันที ความรู้สึกนึกคิด ความฝัน ความตระหนักในตัวตน (self-awareness) ต่างเป็นกิจกรรมที่เกิดขึ้นในสมองทั้งนั้น เมื่อสมองหยุดทำงานลง ก็หมายถึงความตายที่เราคุ้นเคยกันดี
แต่ความตายนั้นเป็นจุดจบจริงหรือ?
แบบจำลองจักรวาลที่ได้รับการยอมรับอย่างมากที่สุด ณ ขณะนี้ เสนอว่าจักรวาลนั้นมีจุดกำเนิด (big bang) เมื่อสิบสี่พันล้านปีที่แล้ว และขยายตัวมาเรื่อยๆ ด้วยอัตราเร่งที่สูงอย่างน่าใจหายถึงทุกวันนี้
หากจักรวาลยังคงขยายตัวเช่นนี้ต่อไป แรงโน้มถ่วงจะเป็นฝ่ายพ่ายแพ้ ท้ายที่สุดแล้วจักรวาลจะเย็นตัวลงเพราะไม่เหลือแหล่งพลังงานอีกต่อไป ดาวทั้งหลายดับสิ้นไปจนมีแต่ความมืดมิดปกคลุมทุกหนแห่ง ช่วงเวลาดังกล่าวจะยาวนานตราบชั่วนิจนิรันดร์และไม่มีวันย้อนกลับ
เมื่อนั้นแหละ ที่ความตายได้กลายเป็นจุดจบอย่างแท้จริง สำหรับชีวิตที่มีโอกาสเพียงแค่หนึ่งครั้ง
Aug 13, 2015
Aug 6, 2015
ตรวจ 2^n
วันก่อนเห็นทวีตนี้ของ @Methuz
แล้วก็มานึกได้ว่าเคยเห็นโจทย์นี้มานานแล้วนะ พอนั่งคิดต่ออีกหน่อยก็พบว่าปรับปรุงให้ดีขึ้นได้อีก โดยเปลี่ยนส่วนที่กลับข้างบิตแล้วเปรียบเทียบกับตัวเอง ไปเป็นไม่ต้องกลับข้างบิตแต่เอาไปเทียบกับศูนย์แทน กล่าวคือ
ถ้าไม่เชี่ยววิเคราะห์อัลกอริทึม ดูแค่นี้อาจยังมองไม่เห็นว่าปรับปรุงดีขึ้นจริงหรือเปล่า เลย compile เป็น assembly ออกมาแล้วไล่ดูทีละบรรทัดเลย
คำสั่งที่ใช้ compile ให้เป็น assembly (GAS syntax) คือ
อันนี้คือผลลัพธ์แบบแรกที่คุณ @Methuz เสนอ (ตัดมาเฉพาะส่วนฟังก์ชันที่สำคัญนะครับ)
และอันนี้เป็นแบบที่ปรับปรุงแล้ว
โอเค ใครอ่านถึงตรงนี้แล้วไม่งงก็กดปิดได้ เพราะเห็นชัดว่าปรับปรุงให้ดีขึ้นจริง
ส่วนใครงงอยู่ เดี๋ยวมาไล่โค้ดกันทีละบรรทัดกันครับ :D
แต่ก่อนอื่น หากอยากอ่าน assembly รู้เรื่อง ก็ต้องไปทำความคุ้นเคยกับ register เสียก่อน (พวกที่เขียนด้วยเครื่องหมาย
ในฟังก์ชันที่ตัดมานี้มี register ที่สำคัญอยู่ 2 ตัว คือ
ดังนั้นหากค่าของ
ส่วนหน้าที่พิเศษของ register ทั้ง 2 ตัวในที่นี้คือ
พอคุ้นเคยกับ register เหล่านี้แล้ว คราวนี้ก็ได้เวลามาดูโค้ดจริงๆ เสียที
บรรทัดแรกนี้สั่งล้างค่า
สองบรรทัดต่อมาเป็นการเช็คว่า
บรรทัดต่อๆ มาเป็นชุดคำสั่งในส่วนหลังที่ถามว่า หากมอง
ชุดบรรทัดพวกนี้มีหน้าที่และผลลัพธ์เหมือนกับชุดบรรทัดในหัวข้อย่อยก่อนเลยครับ สิ่งที่แตกต่างคือวิธีการเท่านั้น โดยโค้ดเริ่มทำงานจากคัดลอกค่า
ถึงตอนนี้การคำนวณหลักๆ ได้เสร็จสิ้นหมดแล้ว ที่ยังไม่เสร็จคือเตรียมค่าที่จะส่งคืน เพราะก่อนหน้านี้เราใช้
สุดท้ายก็ได้เวลาส่งคืนคำตอบครับ สังเกตว่า compiler เพิ่มความมั่นใจให้คำสั่งนี้ด้วยการเติม
สุดท้ายการวิเคราะห์อัลกอริทึม/โค้ดยังไม่พอ นี่คือผลเชิงประจักษ์ของเวลาที่ใช้ไปสำหรับการตรวจว่าเลขดังกล่าวเป็น 2^n หรือไม่ สำหรับจำนวนเต็มบวกทุกตัวที่มีค่าต่ำกว่าสองพันล้านครับ
ก็เร็วต่างกันประมาณ 10%
Best way to check if number is a power of two
return ((x != 0) && ((x & (~x + 1)) == x))
— Midas~♫ (@Methuz) June 24, 2015
แล้วก็มานึกได้ว่าเคยเห็นโจทย์นี้มานานแล้วนะ พอนั่งคิดต่ออีกหน่อยก็พบว่าปรับปรุงให้ดีขึ้นได้อีก โดยเปลี่ยนส่วนที่กลับข้างบิตแล้วเปรียบเทียบกับตัวเอง ไปเป็นไม่ต้องกลับข้างบิตแต่เอาไปเทียบกับศูนย์แทน กล่าวคือ
@Methuz ((x != 0) && ((x & (x - 1)) == 0))
— เนยสดร้องไห้ทำมัย (@neizod) June 24, 2015
ถ้าไม่เชี่ยววิเคราะห์อัลกอริทึม ดูแค่นี้อาจยังมองไม่เห็นว่าปรับปรุงดีขึ้นจริงหรือเปล่า เลย compile เป็น assembly ออกมาแล้วไล่ดูทีละบรรทัดเลย
คำสั่งที่ใช้ compile ให้เป็น assembly (GAS syntax) คือ
$ gcc -S -O3 program.c
อันนี้คือผลลัพธ์แบบแรกที่คุณ @Methuz เสนอ (ตัดมาเฉพาะส่วนฟังก์ชันที่สำคัญนะครับ)
chkpowof2: xorl %eax, %eax testl %edi, %edi je .L42 movl %edi, %eax negl %eax andl %edi, %eax cmpl %eax, %edi sete %al movzbl %al, %eax .L42: rep ret
และอันนี้เป็นแบบที่ปรับปรุงแล้ว
chkpowof2: xorl %eax, %eax testl %edi, %edi je .L42 leal -1(%rdi), %eax testl %edi, %eax sete %al movzbl %al, %eax .L42: rep ret
โอเค ใครอ่านถึงตรงนี้แล้วไม่งงก็กดปิดได้ เพราะเห็นชัดว่าปรับปรุงให้ดีขึ้นจริง
ส่วนใครงงอยู่ เดี๋ยวมาไล่โค้ดกันทีละบรรทัดกันครับ :D
แต่ก่อนอื่น หากอยากอ่าน assembly รู้เรื่อง ก็ต้องไปทำความคุ้นเคยกับ register เสียก่อน (พวกที่เขียนด้วยเครื่องหมาย
%
นำหน้า) ซึ่งมันก็คือตัวแปรต่างๆ นั่นเอง เพียงแต่ว่าตำแหน่งในโลกจริงของ register ถูกวางไว้ใน CPU เพื่อให้ทำงานได้อย่างรวดเร็ว ทำให้ register ทั้งหมดนั้นมีอยู่อย่างจำกัด (ไม่กี่สิบตัว)ในฟังก์ชันที่ตัดมานี้มี register ที่สำคัญอยู่ 2 ตัว คือ
%eax
และ %edi
และยังมี register ตัวอื่นอีก เช่น %al
ซึ่งเป็นส่วนย่อยของ %eax
โดยอธิบายความสัมพันธ์ได้ดังแผนภาพนี้sample value: 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111 a family register: |- %ah -| |- %al -| |------ %ax ------| |--------------- %eax ----------------| |----------------------------------- %rax ------------------------------------| di family register: |------ %di ------| |--------------- %edi ----------------| |----------------------------------- %rdi ------------------------------------|
ดังนั้นหากค่าของ
%eax
(32 บิต) ถูกตั้งไว้เป็น 0x89ABCDEF
เมื่อเรียกดูค่าของ %al
(8 บิต) ก็จะว่ามีค่าเป็น 0xEF
ส่วนหน้าที่พิเศษของ register ทั้ง 2 ตัวในที่นี้คือ
%eax
เก็บค่าที่จะ return จากฟังก์ชัน%edi
เก็บค่า argument ของฟังก์ชัน
พอคุ้นเคยกับ register เหล่านี้แล้ว คราวนี้ก็ได้เวลามาดูโค้ดจริงๆ เสียที
บรรทัด 1
xorl %eax, %eax
บรรทัดแรกนี้สั่งล้างค่า
%eax
ให้เป็นศูนย์ไว้ก่อน โดยใช้เทคนิคว่า ค่าใดๆ เมื่อนำไป xor กับตัวเองแล้วจะได้ศูนย์เสมอ ท่านี้จะเร็วกว่าการโหลดข้อมูลจาก memory มาถมบรรทัด 2-3:
testl %edi, %edi je .L42
สองบรรทัดต่อมาเป็นการเช็คว่า
%edi
(ตัวแปร x
) มีค่าเป็นศูนย์หรือเปล่า โดยใช้เทคนิคว่า ค่าใดๆ เมื่อนำไป and กับตัวเองก็จะได้ตัวเองเสมอ หากตรวจแล้วพบว่าเป็นศูนย์ก็ให้กระโดดไปยังจุดจบฟังก์ชัน ซึ่งเมื่อรวมกับข้อมูลก่อนหน้าว่าตอนนี้ %eax
เป็นศูนย์อยู่ ก็คือฟังก์ชันนี้ให้คำตอบเป็น false
นั่นเองบรรทัด 4-8 ของแบบแรก:
movl %edi, %eax negl %eax andl %edi, %eax cmpl %eax, %edi sete %al
บรรทัดต่อๆ มาเป็นชุดคำสั่งในส่วนหลังที่ถามว่า หากมอง
x
เป็น binary แล้ว จะมีตัวเลข 1 เพียงตัวเดียวเท่านั้นหรือเปล่า? ซึ่งโค้ดชุดนี้ทำงานโดยเริ่มจากการคัดลอกค่า x
จาก %edi
มาไว้ที่ %eax
ก่อน แล้วหาค่าติดลบแบบ two complement ของ %eax
และเก็บไว้ที่เดิม (มีค่าเทียบได้กับการคำนวณ ~x+1
) แล้วจึงเอาค่าของ %edi
ไปทำการ and และเก็บค่าไว้ใน %eax
(ตอนนี้ %eax
ก็จะเก็บค่าของ x&(~x+1)
แล้ว) สุดท้ายถามว่า %eax
ลบ %edi
ได้ศูนย์หรือเปล่า ถ้าใช่ก็จะตั้งค่า %al
ให้เท่ากับหนึ่ง (ซึ่งก็คือ ฟังก์ชันจะคืนค่า true
)บรรทัด 4-6 ของแบบสอง:
leal -1(%rdi), %eax testl %edi, %eax sete %al
ชุดบรรทัดพวกนี้มีหน้าที่และผลลัพธ์เหมือนกับชุดบรรทัดในหัวข้อย่อยก่อนเลยครับ สิ่งที่แตกต่างคือวิธีการเท่านั้น โดยโค้ดเริ่มทำงานจากคัดลอกค่า
x
จาก %rdi
มาลบหนึ่งระหว่างทางแล้วเอาผลลัพธ์ไปเก็บไว้ใน %eax
(ตรงนี้เนื่องจากมีการคำนวณพื้นฐานอย่างบวกลบ ทำให้ compiler รวบคำสั่งเข้ามาที่เดียว ต่างจากการคำนวณซับซ้อนอย่างการหาค่าติดลบครับ) เรียบร้อยแล้วเอา %edi
มา and กับ %eax
หากได้ศูนย์ก็จะตั้งค่า %al
ให้เท่ากับหนึ่ง (คืน true
)บรรทัดรองสุดท้าย
movzbl %al, %eax
ถึงตอนนี้การคำนวณหลักๆ ได้เสร็จสิ้นหมดแล้ว ที่ยังไม่เสร็จคือเตรียมค่าที่จะส่งคืน เพราะก่อนหน้านี้เราใช้
%al
ยาว 8 บิต แต่ตอนส่งค่าคืนต้องใช้ %eax
ที่ยาว 32 บิต บรรทัดนี้จะช่วยแปลงค่าดังกล่าวบรรทัดสุดท้าย
rep ret
สุดท้ายก็ได้เวลาส่งคืนคำตอบครับ สังเกตว่า compiler เพิ่มความมั่นใจให้คำสั่งนี้ด้วยการเติม
rep
ไว้ข้างหน้า เพื่อให้โค้ดที่ทำงานมาถึงบรรทัดนี้ทำคำสั่ง ret
ซ้ำๆ จนกว่าจะสำเร็จนั่นเองสุดท้ายการวิเคราะห์อัลกอริทึม/โค้ดยังไม่พอ นี่คือผลเชิงประจักษ์ของเวลาที่ใช้ไปสำหรับการตรวจว่าเลขดังกล่าวเป็น 2^n หรือไม่ สำหรับจำนวนเต็มบวกทุกตัวที่มีค่าต่ำกว่าสองพันล้านครับ
$ gcc --version gcc (Ubuntu 4.9.2-10ubuntu13) 4.9.2 $ cat /proc/cpuinfo ... model name : Intel(R) Core(TM) i5-4200U CPU @ 1.60GHz ... $ time method-1 # ((x != 0) && ((x & (~x + 1)) == x)) real 0m0.234s ~ 0m0.289s user 0m0.232s ~ 0m0.288s sys 0m0.000s $ time method-2 # ((x != 0) && ((x & (x - 1)) == 0)) real 0m0.212s ~ 0m0.262s user 0m0.208s ~ 0m0.260s sys 0m0.000s
ก็เร็วต่างกันประมาณ 10%
Subscribe to:
Posts (Atom)