Tutorial

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

By Arnon Puitrakul - 19 ธันวาคม 2022

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 เลย)

Compiling Language

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

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

Interpreter Language

ทำให้เกิดพวกภาษาที่ใช้งานกลุ่มของ 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 ลงไปได้พอสมควรเลยทีเดียว

Read Next...

จัดการข้อมูลบน Pandas ยังไงให้เร็ว 1000x ด้วย Vectorisation

จัดการข้อมูลบน Pandas ยังไงให้เร็ว 1000x ด้วย Vectorisation

เวลาเราทำงานกับข้อมูลอย่าง Pandas DataFrame หนึ่งในงานที่เราเขียนลงไปให้มันทำคือ การ Apply Function เข้าไป ถ้าข้อมูลมีขนาดเล็ก มันไม่มีปัญหาเท่าไหร่ แต่ถ้าข้อมูลของเราใหญ่ มันอีกเรื่องเลย ถ้าเราจะเขียนให้เร็วที่สุด เราจะทำได้โดยวิธีใดบ้าง วันนี้เรามาดูกัน...

ปั่นความเร็ว Python Script เกือบ 700 เท่าด้วย JIT บน Numba

ปั่นความเร็ว Python Script เกือบ 700 เท่าด้วย JIT บน Numba

Python เป็นภาษาที่เราใช้งานกันเยอะมาก ๆ เพราะความยืดหยุ่นของมัน แต่ปัญหาของมันก็เกิดจากข้อดีของมันนี่แหละ ทำให้เมื่อเราต้องการ Performance แต่ถ้าเราจะบอกว่า เราสามารถทำได้ดีทั้งคู่เลยละ จะเป็นยังไง เราขอแนะนำ Numba ที่ใช้งาน JIT บอกเลยว่า เร็วขึ้นแบบ 700 เท่าตอนที่ทดลองกันเลย...

Humanise the Number in Python with "Humanize"

Humanise the Number in Python with "Humanize"

หลายวันก่อน เราทำงานแล้วเราต้องการทำงานกับตัวเลขเพื่อให้มันอ่านได้ง่ายขึ้น จะมานั่งเขียนเองก็เสียเวลา เลยไปนั่งหา Library มาใช้ จนไปเจอ Humanize วันนี้เลยจะเอามาเล่าให้อ่านกันว่า มันทำอะไรได้ แล้วมันล่นเวลาการทำงานของเราได้ยังไง...

ทำไม 0.3 + 0.6 ถึงได้ 0.8999999 กับปัญหา Floating Point Approximation

ทำไม 0.3 + 0.6 ถึงได้ 0.8999999 กับปัญหา Floating Point Approximation

การทำงานกับตัวเลขทศนิยมบนคอมพิวเตอร์มันมีความลับซ่อนอยู่ เราอาจจะเคยเจอเคสที่ เอา 0.3 + 0.6 แล้วมันได้ 0.899 ซ้ำไปเรื่อย ๆ ไม่ได้ 0.9 เพราะคอมพิวเตอร์ไม่ได้มองระบบทศนิยมเหมือนกับคนนั่นเอง บางตัวมันไม่สามารถเก็บได้ เลยจำเป็นจะต้องประมาณเอา เราเลยเรียกว่า Floating Point Approximation...