C เร็วกว่า Python จริง ๆ เหรอ ?

ช่วงหลัง ๆ เราชอบเข้าไปอ่านในพวกกลุ่ม Facebook หรือใน Raddit เอง เราก็มักจะเจอประเด็นพวกเรื่องว่า ภาษาอะไรทำงานได้เร็วกว่าอีกภาษานึง แต่ภาษาที่เหมือนจะโดนเสียดแทงมากที่สุด เห็นจะหนีไม่พ้น C และ Python วันนี้เราจะมาทดลองดูกันว่า ที่เขาพูดกันเป็นเรื่องจริงมั้ย และ อะไรคือเบื้องหลังที่ทำให้เกิดผลแบบนั้นขึ้นกัน

2 ภาษานี้มีการทำงานที่แตกต่างกันโดยสิ้นเชิง

ก่อนที่เราจะไปบอกว่า อะไรเร็วกว่า อะไรช้ากว่า เราขอเริ่มเล่าจากวิธีการทำงานของทั้ง 2 ภาษานี้กันก่อน

ภาษา C ถือว่าเป็นภาษาที่ค่อนข้างมีอายุมาก ๆ แต่ก็ยังมีการใช้งานอย่างแพร่หลายจนถึงปัจจุบัน ลามไปตั้งแต่ Embedded System ขนาดเล็ก ๆ ยัน HPC ถ้าบอกว่า HPC ไม่ใช้ตรบ เพราะเราก็เขียน Machine Learning ใช้ C เหมือนกัน จบนะ

ซึ่งต้องยอมรับว่า ณ ขณะที่ภาษา C มันเกิดขึ้นมา พวก Performance ของเครื่องคอมพิวเตอร์มันก็ไม่ได้มีความสามารถที่ทำงานได้อย่างรวดเร็วเหมือนทุกวันนี้ และ คอมพิวเตอร์ก็ไม่เข้าใจภาษา Programming และ คนก็ยากที่จะเข้าใจภาษาเครื่องด้วย ทำให้เกิดโปรแกรมนึงขึ้นมา ที่เราเรียกว่า Compiler ขึ้น ซึ่งตอนนั้นเอง การแปลง หรือการ Compile Code ทั้งหลาย มันเป็นงานที่กินทรัพยากรเครื่องสูงมาก ๆ (จนทุก ๆ วันนี้โปรแกรมใหญ่ ๆ หน่อยอย่าง Chromium และ Linux เองก็ยังใช้เวลาสักพักในการ Compile เลย)

สิ่งที่ Compiler ทำ มันจะทำการแปลง Source Code หรือ Code ที่เราเขียนลงไปให้เป็นภาษาที่เครื่องเข้าใจและทำงานได้เลย ไฟล์พวกนี้เราเรียกว่า Executable File เมื่อเราเรียกโปรแกรมขึ้นมา เครื่องก็สามารถอ่าน และทำตามโปรแกรมที่มันผ่าน Compiler ได้โดยตรงเลย แต่ข้อเสียคือ ภาษาเครื่องแต่ละเครื่องดันไม่เหมือนกันอีก ยิ่งสมัยก่อน เรามี CPU หลากหลาย Platform มากกว่าตอนนี้มาก ๆ ก็คือ ชิบหายวายป่วง ต้องมานั่ง Compile ไป ๆ มา ๆ อีก

จนเมื่อคอมพิวเตอร์มีประสิทธิภาพสูงขึ้นและปัญหาของการ Compile ข้าม Platform ก็ยังเกิดขึ้นอยู่เลยทำให้เกิดภาษาที่ทำงานอีกประเภทขึ้นมา จากเดิมที่เราต้องแปลทิ้งไว้เลย เราก็แปลระหว่างทางเอาดีกว่ามั้ย

ทำให้เกิดพวกภาษาที่ใช้งานกลุ่มของ Interpreter ขึ้นมา โดยเมื่อเราจะรันจริง ๆ มันจะไม่ได้แปลง Source Code ของเราเป็นภาษาเครื่องโดยตรง มันจะแปลงเป็นภาษาที่อยู่ตรงกลางก่อน พวกนี้เราเรียกว่า Byte Code และเมื่อเราจะรันโปรแกรมจริง ๆ มันจะมี โปรแกรมอีกตัว เหมือนกับเป็น Virtual Machine ถ้าเป็น Java เราก็ต้องทำการติดตั้ง Java Virtual Machine (JVM) ก่อน พวกนี้มันก็จะอ่านแล้วไปสั่งเครื่องโดยตรงอีกทีนึง ทำให้ทุก ๆ คำสั่งที่เกิดขึ้น มันจะต้องมีการแปลงอยู่ตลอดเวลานั่นเอง

ใครเร็วกว่ากัน : Hello World Script

เพื่อให้เห็นภาพที่เราเล่ามากขึ้น เรามาทำการทดลองกันดีกว่า เราเริ่มจากโปรแกรมที่ง่ายที่สุดกันก่อน อย่าง Hello World เราจะทดลองหาเวลากันว่า จาก Source Code จนถึงรันเลยเนี่ย ใครจะเร็วกว่ากัน

#include <stdio.h>

int main () {
    printf("Hello World");
    return 0;
}
hello_world.c

สำหรับฝั่งของภาษา C เราก็ทำการ Implement Hello World ขึ้นมาแบบง่าย ๆ เลย ถ้าใครที่เรียนภาษา C มาก่อนน่าจะพอเข้าใจเนอะ

print("Hello World")
hello_world.py

สำหรับใน Python ด้วยความเป็นภาษาที่สมัยใหม่หน่อยมันก็จะเรียบง่ายกว่ากันเยอะมาก เหลือแค่บรรทัดเดียวรู้เรื่องเลย

import time
import os

start_time = time.perf_counter()
os.system('python hello_world.py')
print('Python Took', time.perf_counter() - start_time, 'sec')

start_time = time.perf_counter()
os.system('gcc hello_world.c -o hello_world && ./hello_world')
print('C Lang Took', time.perf_counter() - start_time, 'sec')

ในการทดลองนี้เราจะเขียน Python Script ออกมาเพื่อวัดเวลาในการรัน โดยที่ฝั่งของ Python เราสามารถรันได้ตรง ๆ เลย แต่ถ้าเป็นภาษา C เอง อย่างที่เราบอกคือ เราจะต้อง Compile ก่อนแล้วจึงค่อยรันโปรแกรมออกมา ทำให้เราต้อง Pipe เพื่อให้มัน Compile และรันออกมานั่นเอง

Python Took 0.022759125 sec
C Lang Took 0.195067708 sec

ผลที่ออกมา เราจะเห็นว่า เหยยยย Python เร็วกว่าเป็น 10 เท่าเลยใช่ป่ะ ดังนั้น จริง ๆ แล้วประโยคที่คนบอกว่า C เร็วกว่า Python ก็ไม่จริงแล้วอะดิ เอ๋ เหรอ เรามาดูกันดีกว่าว่ามันเกิดอะไรขึ้นกันแน่

Compilation Took 0.055378959 sec
Run Took 0.18390583300000002 sec

ตอนแรกที่เราทดลอง เราก็สงสัยต่อว่า แล้วจริง ๆ เวลาที่ C มันช้ากว่า Python เยอะขนาดนั้น มันเกิดจากอะไรกันแน่ ระหว่างการ Compile หรือการ Run โปรแกรมขึ้นมาเลย เราเลยลองแยกฉีกเวลาในการ Compile และ Run ออกจากกัน เราจะเห็นจากผลด้านบนเลยว่า เวลาในการ Run กินไปเกินครึ่งเลย แค่ Run อย่างเดียวยังแพ้ Python เลย

เหตุที่เป็นแบบนี้เราเชื่อว่า ส่วนนึงเป็นพวก Overhead ในการสั่งรัน Application ต่าง ๆ บน OS ด้วยนะ เพราะเวลามันเล็กมาก ๆ จะบอกว่ามันไม่เกี่ยวก็ไม่ได้เหมือนกัน เพราะใน C เราต้องเรียกทั้ง Compiler อย่าง gcc ซึ่งมันก็มี Overhead ของมัน และในโปรแกรมของเราเองมันก็มีของมันเช่นกัน เลยน่าจะทำให้มันต่างกันขนาดนั้น

ใครเร็วกว่ากัน : Mean From Random Number

อย่างที่เราบอกว่า ในกลุ่มของภาษาที่เป็นพวก Interpreter เวลามันทำงาน มันจะต้องมี Overhead ในการทำงานเยอะมาก ๆ เพราะมันจะต้องแปลงเป็น Machine Code ในขณะที่ Code กำลังรันเลย ตัวอย่างที่แล้วมันรันอยู่แค่ให้ Print ข้อความออกทางหน้าจอ ง่ายไป เราลองอะไรที่มันต้องรันเยอะกว่านั้นหน่อยละกัน โจทย์ของเราจะทำการหา Mean ของตัวเลขที่สุ่มออกมาเป็นค่า 0-999 โดยสุ่มทั้งหมด 1 ล้านครั้งด้วยกัน

#include <stdio.h>
#include <stdlib.h>  

int main () {
    int n = 1000000;
    int sum = 0;

    for (int i=0;i<n;i++) {
        sum += rand() % 1000;
    }

    float mean = sum / n;
    return 0;
}

ตัวโปรแกรมเราทำง่ายมาก ๆ เรามีตัวแปร n ไว้เก็บจำนวนที่เราต้องการจะรัน โดยที่เซ็ตไว้ 1 ล้านครั้งด้วยกัน ตัวแปร sum ไว้เก็บผลรวมก่อนที่จะหาร แล้วเราก็ทำ Loop ขึ้นมา ให้นับไปเรื่อย ๆ 1 ล้านครั้ง แล้วใน Loop เราก็จะเอาตัวแปร sum บวกด้วยค่าที่เรา Random ขึ้นมา สุดท้าย ค่า Mean ก็คือ Sum ที่เราหาผลรวมมา หารด้วยจำนวนคือ n เป็นอันจบง่าย ๆ

import random

n = 1_000_000

sum = 0

for _ in range(n) :
    sum += random.randint(0, 1_000)

mean = sum / n

ส่วนของ Python เองเราก็ใช้วิธีเดียวกันเป๊ะ ๆ เลย แต่ความ Python เป็นภาษาสมัยใหม่หน่อย มันก็ทำให้เรา Focus กับ Logic ได้มากขึ้นเยอะ เลยทำให้ Code มันดูสั้นลงเยอะเลย

Python Took 0.5247014999999999 sec
C Lang Took 0.12918779200000008 sec

ผลที่ออกมา สลับกันซะแล้วเพราะ Python ช้ากว่า C ประมาณ 0.4 วินาทีเลย กับ Loop 1 ล้านครั้ง ไหงเป็นงั้นได้ อย่างที่เราบอกคือ ฝั่งของ Python ที่ใช้งาน Interpreter มันจะต้องทำการแปลง Code อยู่ตลอดเวลา ทำให้ถ้าเรามีการรันจำนวนเยอะ ๆ พูดง่าย ๆ เยอะบรรทัด มันก็จะมี Overhead มากขึ้น แต่ใน C ส่วนของแปลงจริง ๆ มันเกิดครั้งเดียว และมันไม่ได้ต้องแปลง 1 ล้านครั้ง มันแค่สั่ง Loop เฉย ๆ เทียบเท่ากับ b.ge ใน Assembly อะไรประมาณนั้นทำให้มันทำงานได้เร็วกว่าเยอะมาก ๆ

จากผลการทดลองของทั้ง 2 โปรแกรมจะทำให้เราเห็นว่า ภาษาไหน ๆ มันก็มี Overhead ในการแปลงทั้งนั้น เพราะภาษาที่เราเข้าใจ และเครื่องเข้าใจ มันไม่ไปด้วยกัน ถ้าเรามานั่งเขียนภาษาเครื่องเราตายแน่ ๆ วัน ๆ กลับไปนั่งบวกเลข ชิบหายแน่นอน แต่สิ่งที่แตกต่างกันก็คือ กลุ่มที่เป็น Compiler อย่าง C เองที่แปลงจาก Source Code เป็น Machine Code หรือภาษาเครื่องตรง ๆ เลย มันจะทำให้เราสามารถลดเวลาในการแปลงได้ ถ้าเรามีการรันหลาย ๆ รอบ เช่น เรา Compile ทิ้งไว้ แล้วมีการเรียกใช้บ่อยมาก ๆ อันนั้นมันก็จะคุ้ม แต่เคสที่ไม่คุ้มก็คือตัวแรกใช่มะ นอกจากนั้น จำนวนครั้งในการสั่งการ ก็มีผลเหมือนกัน เหมือนที่เราเห็นในโปรแกรมแรก ไหง Python เร็วกว่าเฉย เพราะ C เองมันมีต้นทุนในการที่ต้อง Compile ซึ่งใช้เยอะพอสมควร

แต่กลับกัน Python ในโปรแกรมแรกเร็วกว่ามาก ๆ เพราะวิธีการทำงานของมันออกแบบมาให้มีประสิทธิภาพเมื่อเราค่อย ๆ อ่านลงไปเรื่อย ๆ หรือความเร็วในการอ่านต่อบรรทัดมันเร็วกว่านั่นเอง แต่.... สิ่งที่ทำให้แพ้ในโปรแกรมที่ 2 คือ เมื่อมันมีจำนวนครั้งในการสั่งมาก ๆ มันก็จะแพ้อันที่เขาไม่ต้องมานั่งสั่งเป็นล้าน ๆ ครั้ง เขาเขียนให้มัน Jump ไปมาไปเลย ก็ชั้นออ่าน Source Code มาหมดแล้ว ชั้นเห็นแล้วว่า Jump ได้ ชั้นก็สั่ง Jump ไปสิขร๊ะ ทำให้ล่นเวลาได้มหาศาลเลย

How to OVERCOME with Python

สิ่งนึงที่เรามองว่า Python เหนือกว่า C มาก ๆ คือ Simplicity หรือ ความเรียบง่าย ถ้าเราดูง่าย ๆ ดูจากโปรแกรมแรกเอาได้ เราจะเห็นว่า แค่ Hello World มันเขียนอยู่บรรทัดเดียว แต่ C เราเขียนอยู่หลายบรรทัดมาก ๆ ไหนจะพวก Learning Curve ถ้าเราอยากจะแสดงข้อความออกทางหน้าจอ ใน Python โอเค print() จบ แต่ใน C เราต้องเข้าใจ include อะไรเยอะกว่ามาก ๆ แต่ปัญหาคือ Performance เหมือนที่เราทดลองกัน ทำให้เกิดคำถามว่า แล้วเราจะ Overcome ปัญหานี้ได้อย่างไรกันละ

Python Took 0.074832375 sec
C Lang Took 0.128376416 sec

ผลด้านบนเกิดจากโปรแกรมที่ทำเหมือนกับตัวอย่างที่ 2 ทุกประการคือ Random ตัวเลขมา 1 ล้านตัวแล้วเอามาหาค่าเฉลี่ย แต่เอ๊ะ ทำไมรอบนี้ Python มันกิน C ไปแบบเยอะมาก ๆ เลยละ เราเปลี่ยนอะไรเหรอ

import numpy as np

mean = np.random.randint(1000, size=1_000_000).mean()

ใน Python เราแก้ใหม่ เราเขียนเหลือแค่นี้เลย เราใช้ Library อย่าง Numpy เข้ามาช่วยบอกให้มัน Random มาล้านตัวแล้วเอามาหา Mean ก็เหมือนกับที่เราเขียนในตัวอย่างก่อนหน้าเลยใช่มะ

แต่ทำไมเวลามันห่างกันได้ขนาดนั้น ทั้ง ๆ ที่เราบอกว่า Python มันมี Overhead ในการแปลงแต่ละ Operation ไม่ใช่เหรอ ถ้าเราเข้าไปดู Source Code ของ Numpy จริง ๆ เราจะเห็นว่า จริง ๆ แล้ว Numpy ที่เป็น Library ที่ทำงานบน Python ไม่ได้ถูกเขียนด้วย Python แต่เป็น C ต่างหาก และ Source Code ที่เขียนด้วย C เหล่านี้ อาจจะถูก Compile ตอนที่เรา Install Library ผ่าน pip หรือ conda ไปแล้ว หรือเผลอ ๆ โหลด Executable ที่ Compile มาแล้วด้วยซ้ำ ดังนั้นการจะรันพวกนี้ตัดเวลาในการ Compile ไปได้เลย นั่นส่วนนึงแล้ว

รันโปรแกรมเร็วขึ้นด้วย SIMD บน Apple Silicon โคตรเร็ว
จะเป็นอย่างไร ถ้าเราบอกว่า เราสามารถเขียนโปรแกรมของเราให้เร็วขึ้นแบบก้าวกระโดด โดยเราไม่ต้องแบ่ง Core ไม่ต้อง Overclock CPU ของเรา แต่เราใช้ประโยชน์จากความสามารถ CPU ของเราได้ ผ่านการทำ SIMD

อีกส่วนหนึ่งเราเดาว่า อย่างใน Function Random ของมัน น่าจะใช้พวกการทำงานแบบ SIMD เข้ามาช่วยด้วยเลยทำให้การ Random มันเร็วแบบมหาศาลมาก ๆ เลยทำให้เกิดตัวเลขที่เราเอาให้ดูขึ้นมาได้

ดังนั้นถามว่า Python มัน Overcome C อย่างไรก็ตอบง่าย ๆ เลยว่า ก็ใช้ C แมร่งเลยสิ เอ๊อ เอาเว้ย มันแค่นั้นเลยจริง ๆ ในเมื่อโค่นล้มไม่ได้ ก็เข้าร่วมมันซะเลย ทำให้จริง ๆ แล้วเราสามารถที่จะเขียน Function บางอย่างใน C และนำมาใช้งานใน Python ได้ ผ่านการเขียนอะไรนิดหน่อยเท่านั้นเอง พูดอีกนัยก็คือ เราสามารถใช้ Python เป็น Interface สำหรับการเรียกใช้งานพวก Function ในภาษา C ได้นั่นเอง

และด้วยความเรียบง่าย และ Learning Curve ที่ต่ำ มาเจอกับการทำ Interface แบบนี้อีก เลยทำให้มันทำเรื่องยาก ๆ ให้ง่ายได้เข้าไปใหญ่เลย แรก ๆ เรามองว่า อาจจะเขียนและทดลอง Logic บน Python ก่อนก็ได้ และ จุดไหนที่เราเอาไปใช้งานจริง ๆ เราก็อาจจะเอาไป Implement บน C แล้วเรียกเข้ามา ทำให้ส่วนอื่น ๆ ของโปรแกรมก็ไม่ต้องเปลี่ยน ก็ใช้งาน Python เหมือนเดิม แต่เราได้ Performance ที่ดีขึ้นแบบเยอะมาก ๆ เหมือนที่เราเห็น หรือจริง ๆ ก็คือ เรียก Library ที่เขาเขียนมาแล้วก็ได้เหมือนกัน

เพิ่มเติม นอกจากการหยิบ Library ที่ทำจาก C เข้ามาช่วยแล้ว การใช้งานพวก PyPy ที่เป็น JIT (Just-in-Time) Compiler ก็จะเข้ามาช่วยได้เยอะมาก ๆ ในบาง Application

สรุป

เอาจริง ๆ ก็คือ ไม่ผิดที่จะบอกว่า ส่วนใหญ่แล้วพวกโปรแกรมที่เขียนจากภาษา C มักจะรันได้เร็วกว่า Python มาก ๆ เพราะเรื่องของวิธีการทำงานที่แตกต่างกัน คือ C จะใช้ Compiler เพื่อ Compile Source Code เป็น Machine Code ทำให้เครื่องอ่านได้ตรง ๆ เลย แต่ Python ใช้งาน Interpreter ทำให้มันเสียเวลาในการแปลงทุกครั้งเมื่อเรารัน ยังไม่นับในโปรแกรมที่ 2 ที่เราทดลองกัน จะเห็นเลยว่า Python เสียเปรียบมาก ๆ เพราะ Interpreter มันอ่านและสั่งเครื่องทีละบรรทัดวนไปเรื่อย ๆ เหมือนกับ Interpreter มองภาพแคบ ๆ ทีละบรรทัด แต่ Compiler มันมองภาพทั้งโปรแกรมในทีเดียว เลยทำให้หลาย ๆ รอบมันมีประสิทธิภาพสูงกว่ามาก ๆ แต่ข้อดีของกลุ่ม Interpreter คือ ความง่ายในการนำไปใช้ในหลาย ๆ Architecture ได้โดยที่เราไม่ต้องไล่ Compile ใหม่ไปเรื่อย ๆ นั่นเอง

ดังนั้นถามว่า ในการใช้งานจริง Python มันไม่ Efficient ขนาดนั้นมั้ย ก็ตอบเลยว่า ก็ใช่.... เมื่อเทียบกับภาษาที่ Compile ทั้งหลายอย่าง C แต่ อย่างที่บอกว่า C พวกนี้มันมี Learning Curve ที่สูงกว่า ความซับซ้อนสูงกว่ามาก ส่งผลไปที่การดูแล และออกแบบที่ยุ่งยากกว่ามาก ทำให้การใช้งาน Python มันได้เรื่องความเรียบง่ายนี่แหละ และเมื่อเราใช้ Python เป็น Interface ในการเข้าถึง C Funtion ได้ ก็ทำให้เราพอจะลดความต่างเรื่อง Performance ลงไปได้พอสมควรเลยทีเดียว