การวิเคราะห์ประสิทธิภาพด้วย timeit และ memory_profiler
วัดเวลาการทำงานด้วย %timeit และหน่วยความจำสูงสุดด้วย memory_profiler เพื่อค้นหาส่วนที่ช้าที่สุดของกระบวนการประมวลผล
การวิเคราะห์ประสิทธิภาพด้วย timeit และ memory_profiler เป็นบทเรียน Pandas & NumPy Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Pandas & NumPy Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Pandas & NumPy Academy มีบทเรียนทั้งหมด 4 บทเรียน
เหตุใดจึงควรทำโปรไฟล์ก่อนปรับประสิทธิภาพ
ข้อผิดพลาดที่พบบ่อยคือการปรับประสิทธิภาพโค้ดตามความรู้สึก เช่น เขียนฟังก์ชันใหม่เพียงเพราะดูเหมือนว่าจะช้า โดยไม่ได้วัดว่าฟังก์ชันนั้นเป็นคอขวดจริงหรือไม่ ในกระบวนการประมวลผลข้อมูล เวลาดำเนินการ 90% มักมาจากโค้ดเพียง 10% การทำโปรไฟล์ช่วยระบุได้อย่างชัดเจนว่าใช้เวลาและหน่วยความจำไปกับส่วนใด เพื่อให้คุณมุ่งเน้นการปรับประสิทธิภาพไปยังจุดที่สำคัญ หลักสำคัญคือ วัดผลก่อน แล้วจึงปรับประสิทธิภาพ เครื่องมืออย่าง timeit ใช้วัดเวลา ส่วน memory_profiler ใช้วัด RAM
import pandas as pd
import numpy as np
import timeit
# Naive approach vs vectorised approach — which is faster?
np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(100000), 'b': np.random.randn(100000)})
t1 = timeit.timeit(lambda: df['a'] + df['b'], number=100)
t2 = timeit.timeit(lambda: [a + b for a, b in zip(df['a'], df['b'])], number=10)
print(f'Vectorised (100 runs): {t1:.3f}s')
print(f'List comprehension (10 runs): {t2:.3f}s')
print(f'Per-run speedup: ~{(t2/10)/(t1/100):.0f}x')timeit: ตัวจับเวลาในตัวของ Python
มอดูล timeit วัดเวลาดำเนินการโดยเรียกใช้คำสั่งซ้ำหลายครั้ง แล้วส่งคืนเวลาที่ผ่านไปทั้งหมด timeit.timeit(stmt, number=n) เรียกใช้ stmt จำนวน n ครั้ง และส่งคืนเวลาเป็นวินาทีทั้งหมด ให้นำเวลานี้หารด้วย number เพื่อหาเวลาเฉลี่ยต่อครั้ง สำหรับการวัดที่แม่นยำ ให้เลือก number เพื่อให้การทำงานทั้งหมดใช้เวลาอย่างน้อย 0.1 วินาที เพราะการเรียกใช้การดำเนินการที่รวดเร็วเพียงครั้งเดียวไม่เป็นตัวแทนที่ดี เนื่องจากสัญญาณรบกวนจากการจัดตารางเวลาของระบบ
import timeit
import pandas as pd
import numpy as np
np.random.seed(0)
df = pd.DataFrame({'x': np.random.randn(10000)})
# Compare three ways to square a column
time_a = timeit.timeit(lambda: df['x'] ** 2, number=1000)
time_b = timeit.timeit(lambda: df['x'].apply(lambda v: v**2), number=100)
time_c = timeit.timeit(lambda: np.square(df['x']), number=1000)
print(f'Operator **2 per run: {time_a/1000*1000:.3f} ms')
print(f'.apply(v**2) per run: {time_b/100*1000:.3f} ms')
print(f'np.square() per run: {time_c/1000*1000:.3f} ms')Magic %timeit ใน Jupyter
ในสมุดบันทึก Jupyter magic %timeit จะเลือกจำนวนการทำซ้ำและจำนวนรอบโดยอัตโนมัติ เพื่อให้ได้ค่าประมาณที่มีเสถียรภาพทางสถิติ จากนั้นรายงานเวลาที่ดีที่สุดและเวลาเฉลี่ยพร้อมส่วนเบี่ยงเบนมาตรฐาน วิธีนี้เป็นวิธีที่สะดวกที่สุดในการทดสอบประสิทธิภาพของบรรทัดเดียวอย่างรวดเร็ว ใช้ %%timeit (เครื่องหมายเปอร์เซ็นต์สองตัว) เพื่อจับเวลาทั้งเซลล์ รูปแบบผลลัพธ์คือ X ns/μs/ms ± Y ns per loop (mean ± std. dev. of 7 runs, N loops each)
# In Jupyter notebook:
# %timeit df['x'] ** 2
# Output: 47.3 us +- 1.2 us per loop (mean +- std. dev. of 7 runs, 10000 loops each)
# Multi-line cell timing:
# %%timeit
# result = df['x'] ** 2
# total = result.sum()
# In a regular Python script, use timeit.timeit() instead:
import timeit
import pandas as pd, numpy as np
df = pd.DataFrame({'x': np.random.randn(10000)})
result = timeit.repeat(lambda: df['x']**2, number=1000, repeat=7)
print(f'Best run: {min(result)/1000*1000:.3f} ms per call')cProfile สำหรับการทำโปรไฟล์ระดับฟังก์ชัน
ขณะที่ timeit ใช้วัดนิพจน์เดียว cProfile จะทำโปรไฟล์สแตกการเรียกใช้ทั้งหมด โดยแสดงว่าใช้เวลาไปกับทุกฟังก์ชันที่ถูกเรียกระหว่างการทำงานมากน้อยเพียงใด วิธีนี้จำเป็นสำหรับการระบุคอขวดในกระบวนการประมวลผลที่ซับซ้อน เรียกใช้ python -m cProfile -s cumulative script.py จากบรรทัดคำสั่ง หรือใช้ cProfile.run('function()') ในสคริปต์ ใน Jupyter ให้ใช้ %prun function()
import cProfile
import pandas as pd
import numpy as np
np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(50000), 'b': np.random.randn(50000)})
def slow_pipeline(df):
result = []
for _, row in df.iterrows():
result.append(row['a'] * row['b'])
return pd.Series(result)
# Profile the function
print('Profile output (top 5 functions by cumulative time):')
cProfile.run('slow_pipeline(df.head(1000))', sort='cumulative')การวัดการใช้หน่วยความจำ
ใช้ df.memory_usage(deep=True) เพื่อดูว่าแต่ละคอลัมน์ใช้ RAM มากเพียงใด อาร์กิวเมนต์ deep=True จะวัดหน่วยความจำจริงที่คอลัมน์ชนิดข้อมูลอ็อบเจ็กต์ (สตริง) ใช้ มิฉะนั้นระบบจะรายงานเพียงขนาดของตัวชี้ ใช้ .sum() เพื่อหาหน่วยความจำทั้งหมดของ DataFrame ในหน่วยไบต์ หารด้วย 1e6 เพื่อแปลงเป็น MB หรือหารด้วย 1e9 เพื่อแปลงเป็น GB นี่คือจุดเริ่มต้นของการปรับการใช้หน่วยความจำให้มีประสิทธิภาพ เพราะคุณไม่สามารถลดสิ่งที่ยังไม่ได้วัดได้
import pandas as pd
import numpy as np
np.random.seed(0)
df = pd.DataFrame({
'id': range(100000),
'name': ['user_' + str(i) for i in range(100000)],
'score': np.random.randn(100000),
'category': np.random.choice(['A', 'B', 'C', 'D'], 100000)
})
mem = df.memory_usage(deep=True)
print('Memory per column (bytes):')
print(mem)
print(f'\nTotal: {mem.sum() / 1e6:.2f} MB')memory_profiler สำหรับหน่วยความจำทีละบรรทัด
แพ็กเกจ memory_profiler (pip install memory-profiler) วัดการใช้ RAM ทีละบรรทัดภายในฟังก์ชัน ใส่ตัวตกแต่ง @profile ให้ฟังก์ชัน แล้วเรียกใช้ด้วย python -m memory_profiler script.py ใน Jupyter ให้ใช้ magic %memit (จากส่วนขยาย ipython) เพื่อวัดหน่วยความจำสูงสุดของนิพจน์เดียว วิธีนี้ช่วยระบุว่าโครงสร้างข้อมูลใดทำให้หน่วยความจำเพิ่มขึ้นอย่างฉับพลัน ซึ่งมักเกิดจากการนำข้อมูลมาต่อกันในลูป แทนที่จะรวบรวมข้อมูลแล้วต่อกันเพียงครั้งเดียว
# Install: pip install memory-profiler
# Usage as a decorator:
# from memory_profiler import profile
# @profile
# def process_data():
# import pandas as pd, numpy as np
# df = pd.DataFrame({'x': np.random.randn(1000000)})
# result = df['x'].rolling(100).mean() # <-- sees if this spikes RAM
# return result
# Run: python -m memory_profiler script.py
# In Jupyter (after %load_ext memory_profiler):
# %memit pd.DataFrame({'x': np.random.randn(1000000)}).rolling(100).mean()
print('memory_profiler decorates functions for line-by-line RAM measurement.')
print('Typical output: Line # Mem usage Increment Line Contents')sys.getsizeof เทียบกับ memory_usage สำหรับอ็อบเจ็กต์
sys.getsizeof(obj) ส่งคืนขนาดหน่วยความจำของอ็อบเจ็กต์ Python แต่สำหรับ DataFrame ของ pandas จะรายงานเพียงค่าใช้จ่ายของโครงสร้างเก็บข้อมูล (ไม่กี่ร้อยไบต์) ไม่ใช่ข้อมูลจริง ให้ใช้ df.memory_usage(deep=True).sum() สำหรับ DataFrame เสมอ สำหรับอ็อบเจ็กต์ Python แต่ละรายการ เช่น ลิสต์และพจนานุกรม sys.getsizeof ให้ค่าที่แม่นยำ แต่จะไม่ไล่ตรวจอ็อบเจ็กต์ที่ซ้อนอยู่ภายใน ให้ใช้ไลบรารี pympler (asizeof) เพื่อหาขนาดอ็อบเจ็กต์เชิงลึก
import sys
import pandas as pd
import numpy as np
df = pd.DataFrame({'x': np.random.randn(100000)})
# sys.getsizeof reports only the container metadata — misleading!
print('sys.getsizeof(df):', sys.getsizeof(df), 'bytes (misleading!)')
# memory_usage gives the true memory
true_mem = df.memory_usage(deep=True).sum()
print('df.memory_usage(deep=True).sum():', true_mem, 'bytes')
print(f'True memory: {true_mem/1e6:.2f} MB')รูปแบบตัวจัดการบริบทสำหรับจับเวลา
สำหรับขั้นตอนในกระบวนการประมวลผลที่ใช้เวลานาน ซึ่ง timeit ไม่เหมาะสม (เช่น การสืบค้นฐานข้อมูลหรืออินพุต/เอาต์พุตไฟล์) ให้ใช้ตัวจัดการบริบทอย่างง่ายร่วมกับ time.perf_counter() ครอบแต่ละขั้นตอนของกระบวนการประมวลผลด้วยบริบทจับเวลาเพื่อบันทึกระยะเวลาที่ใช้ วิธีนี้ใช้งานได้จริงกว่า timeit สำหรับโค้ดในระบบจริง เพราะบันทึกเวลานาฬิกาจริงที่รวมเวลารออินพุต/เอาต์พุตด้วย แม้ timeit จะวัดเวลานี้เช่นกัน แต่ผสานเข้ากับเฟรมเวิร์กการบันทึกข้อมูลได้ยากกว่า
import time
import contextlib
import pandas as pd
import numpy as np
@contextlib.contextmanager
def timer(name):
start = time.perf_counter()
yield
elapsed = time.perf_counter() - start
print(f'[{name}] {elapsed*1000:.1f} ms')
np.random.seed(0)
df = pd.DataFrame({'x': np.random.randn(500000), 'y': np.random.randn(500000)})
with timer('rolling mean'):
result1 = df['x'].rolling(30).mean()
with timer('groupby mean'):
df['group'] = df['x'].round(0)
result2 = df.groupby('group')['y'].mean()การทำโปรไฟล์การดำเนินการของ Pandas อย่างเป็นระบบ
ขั้นตอนการทำโปรไฟล์อย่างเป็นระบบ: 1) เริ่มจากตัวอย่างข้อมูลที่เป็นตัวแทนของข้อมูลทั้งหมด (10,000 แถวก็เพียงพอสำหรับเปรียบเทียบเวลา) 2) ระบุขั้นตอนที่ช้าที่สุดโดยใช้บริบทตัวจับเวลาหรือ cProfile 3) เขียนขั้นตอนที่ช้าที่สุดใหม่ด้วยแนวทางที่เร็วกว่า (การประมวลผลแบบเวกเตอร์ ชนิดข้อมูล Categorical หรือส่วนขยาย C) 4) ตรวจสอบความถูกต้องด้วยคำสั่ง assert 5) จับเวลาอีกครั้งเพื่อยืนยันว่าทำงานเร็วขึ้น 6) ทำซ้ำกับคอขวดถัดไปจนกว่าไปป์ไลน์จะเป็นไปตาม SLA
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({
'value': np.random.randn(100000),
'category': np.random.choice(['A', 'B', 'C', 'D', 'E'], 100000)
})
# Benchmark two groupby implementations
t1 = timeit.timeit(lambda: df.groupby('category')['value'].mean(), number=100)
df2 = df.copy()
df2['category'] = df2['category'].astype('category')
t2 = timeit.timeit(lambda: df2.groupby('category')['value'].mean(), number=100)
print(f'Object dtype groupby: {t1/100*1000:.2f} ms per call')
print(f'Categorical dtype: {t2/100*1000:.2f} ms per call')
print(f'Speedup: {t1/t2:.1f}x')ควรทำโปรไฟล์เมื่อใด: เกณฑ์ใช้งานจริง
ไม่ใช่ทุกสคริปต์ที่จำเป็นต้องทำโปรไฟล์ ควรลงทุนทำโปรไฟล์เมื่อ: ไปป์ไลน์ใช้เวลาทำงานเกิน 30 วินาทีและทำงานเป็นประจำ (วันละครั้งขึ้นไป) ฟังก์ชันประมวลผลมากกว่า 100,000 แถว หรือการใช้หน่วยความจำเกิน 50% ของ RAM ที่มีอยู่ สำหรับสคริปต์ที่ทำงานเพียงครั้งเดียวหรือเสร็จภายในไม่กี่วินาที ความอ่านง่ายสำคัญกว่าการปรับปรุงประสิทธิภาพระดับไมโครวินาที ต้นทุนการดูแลโค้ดที่ปรับแต่งอย่างหนักและอ่านเข้าใจได้ยาก มักสูงกว่าเวลาที่ประหยัดได้จากการปรับปรุงนั้น
ภาพรวม: สรุปเครื่องมือทำโปรไฟล์
เอกสารอ้างอิงฉบับย่อ:
- timeit.timeit — วัดเวลาของคำสั่งเดียว (สคริปต์)
- %timeit / %%timeit — วัดเวลาใน Jupyter (ทำซ้ำโดยอัตโนมัติ)
- %prun / cProfile — ทำโปรไฟล์ลำดับการเรียกใช้ในระดับฟังก์ชัน
- df.memory_usage(deep=True) — วัดการใช้ RAM แยกตามคอลัมน์
- memory_profiler @profile — วัดการใช้ RAM ทีละบรรทัด
- time.perf_counter() — ตัวจับเวลาสำหรับขั้นตอนต่าง ๆ ของไปป์ไลน์
ตรวจสอบความเข้าใจอย่างรวดเร็ว
ทดสอบความเข้าใจเกี่ยวกับเครื่องมือทำโปรไฟล์จากบทเรียนนี้
ทบทวนบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้ว่า timeit.timeit และ %timeit ใช้วัดเวลาการทำงานของการดำเนินการเฉพาะอย่าง df.memory_usage(deep=True) ใช้วัด RAM แยกตามคอลัมน์ได้อย่างแม่นยำ และ cProfile / memory_profiler ช่วยทำโปรไฟล์เชิงลึกสำหรับไปป์ไลน์ที่ซับซ้อน ควรทำโปรไฟล์ก่อนปรับปรุงประสิทธิภาพเสมอ และตรวจสอบความถูกต้องหลังการเปลี่ยนแปลงแต่ละครั้ง บทถัดไปเราจะเรียนรู้วิธีหลีกเลี่ยงลูป iterrows ที่ช้า และแทนที่ด้วยการดำเนินการแบบเวกเตอร์
คำถามที่พบบ่อย
บทเรียน “การวิเคราะห์ประสิทธิภาพด้วย timeit และ memory_profiler” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การวิเคราะห์ประสิทธิภาพด้วย timeit และ memory_profiler” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Pandas & NumPy Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Pandas & NumPy Academy มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การวิเคราะห์ประสิทธิภาพด้วย timeit และ memory_profiler”
วัดเวลาการทำงานด้วย %timeit และหน่วยความจำสูงสุดด้วย memory_profiler เพื่อค้นหาส่วนที่ช้าที่สุดของกระบวนการประมวลผล คุณปฏิบัติ Pandas & NumPy Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Pandas & NumPy Academy หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Pandas & NumPy Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน
บทเรียน “การวิเคราะห์ประสิทธิภาพด้วย timeit และ memory_profiler” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Pandas & NumPy Academy นี้ได้ไหม
ได้ บทเรียน Pandas & NumPy Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การวิเคราะห์ประสิทธิภาพด้วย timeit และ memory_profiler
- การหลีกเลี่ยง iterrows และลูป Python
- ชนิดข้อมูลที่มีประสิทธิภาพเพื่อลดการใช้หน่วยความจำ
- การอ่านไฟล์ขนาดใหญ่เป็นชิ้น