May 28, 2014

อย่าเสียเวลาให้กับลูปที่เรียก System บ่อยๆ

ช่วงนี้มีวิเคราะห์ string บนไฟล์จำนวนมหาศาล (หลายแสนไฟล์) ตอนแรกก็เขียน Python ง่ายๆ ตามความเคยชิน
counter = 0
for file in glob('*.cpp'):
    if '//' in file.read():
        counter += 1
print(counter)
โค้ดนี้ทำงานได้ดีเลยแหละ ติดตรงที่ไฟล์ที่นำมาวิเคราะห์บางไฟล์ก็ใช้ encoding แปลกๆ แล้วเปิดอ่านไม่ได้ (ยังงงอยู่ว่า Python ไม่เช็คว่าเป็น ASCII หรือ UTF-8 ให้อัติโนมัติ?) ก็แก้ๆ โค้ดด้านบนนี้โดยใช้โมดูล codecs เข้าช่วย ไม่มีปัญหาอะไร

แต่แล้วก็คิดขึ้นมาได้ว่า งานประเภทนี้ถ้าทำใน Bash ไปเลยก็น่าจะแจ่มเหมือนกัน เพราะปรัชญาของ Unix คือให้โปรแกรมจัดการกับ I/O Stream แบบข้อความอยู่แล้ว เลยได้โค้ดออกมาเป็น
counter=0
for file in *.cpp
do
    grep -q '//' "$file" && : $((counter++))
done
echo $counter
ผลลัพท์ออกมาไม่ต่างกับ Python ด้านบน แต่ว่า Bash ทำงานช้ามากกกกก (ก.ไก่ 140 ตัว) ตอนแรกก็คิดว่า Bash มันน่าจะทำลูปไม่เก่งมั้ง เพราะเท่าที่อ่านโค้ดของโครงการ Linux ก็ไม่ค่อยเห็นเขียนลูปกันเท่าไหร่ แต่พอลองเขียนลูปเปล่าแสนรอบใน Bash มันก็ไม่เห็นจะช้า เลยคิดว่าน่าจะเป็นที่ grep มั้ง? แต่ก็ไม่อยากจะเชื่ออยู่ดีเพราะเคยอ่านงานของ Russ Cox ก็ไม่เห็นว่า grep มันจะช้าซักเท่าไหร่

อาจารย์ @juggapong ผ่านมาเห็นเลยยิงคำถามมาว่า มันช้าเพราะมัวแต่มุดเข้ามุดออก system หรือเปล่า?

ได้ยินดังนั้นเลยใช้ time จับเวลาดู โอ้ว user กับ sys ใช้เวลาพอๆ กันเลยแฮะ

ถึงตอนนี้ก็รู้แล้วว่าสาเหตุที่มันช้าเพราะ grep แต่ละครั้งเป็น process ใหม่เลย ต้องรอให้ system จองทรัพยากรเสร็จก่อนจึงจะใช้งานได้ แถมพอจบงานก็ทำลาย process นี้ทิ้งซะอีก ไม่ได้จัดสรรทรัพยากรอะไรกันระหว่างลูปเลย ... เทียบง่ายๆ คงเหมือนกับการที่มีเตารีดแสนอัน เสียบปลั๊กเตารีดตัวแรกรอร้อนแล้วจึงรีดผ้าหนึ่งชิ้น พอผ้าชิ้นนี้เรียบเนี๊ยบจนพอใจก็ถอดปลั๊กเตารีดซะงั้น แล้วไปเสียบปลั๊กเตารีดอีกตัวมารีดผ้าอีกชิ้นนั่นเอง

คิดได้ดังนี้ก็เปลี่ยนโค้ดไปเป็น (ใช้เตารีดตัวเดียว เสียบปลั๊กรอร้อนครั้งเดียว)
grep -l '//' *.cpp | wc -l
คราวนี้เร็วส์เสียยิ่งกว่า Python อีก :P

(แต่สุดท้ายก็คงกลับไปเขียน Python อยู่ดี เพราะมีปัจจัยที่ใช้วิเคราะห์ซับซ้อนกว่านี้ ใช้ Python แล้วรู้สึกถนัดมือกว่า)

May 25, 2014

ที่ทางของคำสั่ง : ใน Shell

เคยเขียน (มั้ง) มาทีละว่า ทุกคำสั่งใน linux มันมีที่ทางของมันหมด แม้จะเป็นคำสั่งเล็กๆ ที่ทำหน้าที่ง่อยๆ ก็ตามที ... แต่วันนี้จะมานำเสนอคำสั่ง : (colon ตัวเดียว) ซึ่งเป็นคำสั่งที่ไม่ทำอะไรเลย

อ้าว แล้วเมื่อมันไม่ทำหน้าที่อะไรเลย จะมีไว้ทำไมหละ?

งั้นต้องถามก่อนว่า เคยประสบปัญหานี้มั้ย
> # ตั้งตัวแปรไว้ใช้ดีกว่า
> foo="hahaha"
> # ทำงานอื่นไปเรื่อยๆๆๆๆ
> # เอ๊ะ ตัวแปร $foo โดนเปลี่ยนค่า (?) ไปเป็นอะไรแล้วนะ?
> $foo
hahaha: command not found
ด้านบนนี้เป็น user error เอง อยากดูค่าก็อย่าลืม echo สิ :P

อย่างไรก็ตาม อย่าลืมว่า Shell มันเป็น programming language ตัวหนึ่งเลย ความสามารถที่หลายๆ คนลืมคือเราสามารถทำการคำนวณใน Shell ได้
> i=0
> for f in *
> do
>     [ -s "$f" ] && i=$((i+1))
> done
> echo $i
โค้ดด้านบนนี้จะถามว่าไฟล์ในแฟ้มปัจจุบัน มีกี่ไฟล์ที่มีเนื้อหา (ขนาดไม่เป็น 0 byte) ซึ่ง counter จะเพิ่มขึ้นตรงหลังเครื่องหมาย && นั่นเอง

ในโลกความจริงเราไม่ได้เขียนโค้ดอย่างนี้ตลอด อาจมีบางครั้งที่ต้องเอาการคำนวณแยกไว้ในที่เดี่ยวๆ เช่น
> i=0
> for ...
> do
>     # ทำอะไรซักอย่าง
>     i=$((i+1))
> done
เขียนไปเขียนมาจะพบว่า i=$((i+1)) มันตลกสิ้นดี ยิ่งเมื่อรู้ว่า Shell เขียน i++ ได้! แต่อย่าลืมว่าถ้าเปลี่ยนไปเขียนตรงๆ ก็จะเจอปัญหาเดิม
> $((i++))
0: command not found
ทั่วไปแล้ว การเขียนในแนวนี้จะเป็นการสร้างตัวนับเพื่อบอกว่า process นี้ทำงานไปถึงไหนแล้ว ซึ่งมันสามารถเอาไปแทรกไว้กับคำสั่ง echo ได้
> echo "now doing item number: $((++i))"
แต่ถ้าไม่ต้องการให้มัน echo อะไรระหว่างทางหละ? นี่แหละคือจุดยืนของคำสั่ง :
> : $((i++))
ป.ล. Shell รุ่นเก่าๆ ไม่มีคำสั่ง true เลยต้องใช้คำสั่ง : แทน