Ачааллж байна...
Энэ сургалтын эхнээс бид нэг дүрмийг баримталсан: хурд эсвэл санах ойн тухай ямар ч мэдэгдэл хэмжилтгүйгээр гарахгүй. Хичээл 2.2-т generator expression санах ой хэмнэдэг ч энгийн нийлбэрт list comprehension-оос бага зэрэг удаан байдгийг хэмжсэн. Хичээл 3.3-т lru_cache fib(30)-ийг 0.19 секундээс 21 микросекунд болгосныг хэмжсэн. Хичээл 3.4-т урсгалын pipeline 30 килобайт, бүтэн файлын хувилбар 130 мегабайт хэрэглэсэн гэдгийг хэмжсэн. Энэ бүлэг тэр дүрмийг хэрэгсэл ба сахилга бат болгоно.
Бүлгийн эхний хичээлийн гарчиг нь энэ бүлгийн хууль: хэмжихгүйгээр бүү оновчил. Програм удаан байна гэж мэдрэгдэхэд хөгжүүлэгчийн зөн совин ихэвчлэн шууд "энэ мөр удаан байх" гэж хэлдэг. Тэр зөн совин гайхмаар олон удаа буруу байдаг. Компьютерийн тайлбарлагч, санах ойн бүтэц, Python-ий дотоод оновчлолууд бидний толгойд зурсан загвараас өөрөөр ажилладаг. Буруу таамаглал дээр тулгуурласан "оновчлол" кодыг ойлгомжгүй болгож, хурдыг огт нэмэхгүй, заримдаа бүр удаашруулдаг.
Энэ хичээлд бид Python-ий стандарт хэмжих хэрэгсэл timeit-ийг бүрэн эзэмшинэ: терминалаас ба кодоос, тус бүрийн урхитай нь. Дараа нь хоёр хэмжилт хийнэ, хоёулаа зөн совинг гайхшруулах зорилготой. Эхнийх нь Бүлэг 11-тэй холбоотой: try/except удаан уу? Хоёр дахь нь өдөр тутмын нэг энгийн үйлдлийн нуугдмал зардал. Эцэст нь энэ бүлгийн хэмжилтийн загварыг тогтооно: дараагийн гурван хичээлд бид үүнийг алхам бүрт дагана.
Эмчийн оношийг төсөөлөөд үзээрэй. Өвчтөн "толгой өвдөж байна" гэж ирэхэд туршлагагүй эмч шууд "энэ мигрень" гэж таамаглаад эм бичнэ. Хэрэв шалтгаан нь даралт ихсэлт байсан бол эм тус болохгүй, өвчин үргэлжилнэ, магадгүй эмийн гаж нөлөө нэмэгдэнэ. Туршлагатай эмч эхлээд хэмждэг: даралт, температур, цусны шинжилгээ. Тэр мигрень гэж сэжиглэж болно, гэхдээ сэжиг бол таамаглал, онош биш. Онош хэмжилтээс ирнэ.
Хоёр дахь чухал зүйл: эмчилгээний дараа эмч дахин хэмжинэ. "Эм уусан, одоо сайжирсан байх" гэдэг нь таамаглал. Даралтыг дахин хэмжиж, тоо буурсан эсэхийг шалгана. Хэрэв буураагүй бол эмчилгээ буруу байсан, өөр шалтгааныг хайх хэрэгтэй.
Програмын хурд яг ийм. "Энэ мөр удаан" бол таамаглал. Хэмжилт бол онош. Өөрчлөлт хийсний дараа дахин хэмжих нь эмчилгээ ажилласан эсэхийн баталгаа. Энэ бүлэгт бидний "даралтын аппарат"-ууд бол timeit ба perf_counter, дараагийн хичээлд "цусны шинжилгээ", cProfile нэмэгдэнэ.
Хичээл 2.2-т бид timeit-ийг терминалаас хэрэглэсэн. Тэр бичлэгийг одоо бүрэн задалъя:
python -m timeit -s "names = ['Бат', 'Сараа', 'Тэмүүлэн'] * 1000" "'Тэмүүлэн' in names"20000 loops, best of 5: 13.8 usec per looppython -m timeit нь timeit модулийг програм мэт ажиллуулна. -s нь setup: хэмжилтийн өмнө нэг удаа ажилладаг код, тэр хугацаа хэмжилтэд орохгүй. Сүүлийн хашилт дахь код бол хэмжигдэх statement. Гаралтыг уншъя: “20000 loops, best of 5” нь “statement-ийг 20,000 удаа ажиллуулах цувааг 5 удаа давтав” гэсэн утгатай, “13.8 usec per loop” нь "хамгийн сайн цувааны нэг ажиллуулалт 13.8 микросекунд". usec бол микросекунд, нэг секундийн саяны нэг. timeit цувааны уртыг өөрөө сонгодог: statement хурдан бол олон удаа, удаан бол цөөн удаа ажиллуулж, хэмжилтийн нийт хугацааг 0.2 секундээс дээш байлгана. Python 3.15-д timeit-ийн гаралт өнгөтэй болсон бөгөөд агуулга нь ижил.
Setup ба statement-ийг тусгаарлах нь яагаад чухал вэ? Хэрэв бид жагсаалт үүсгэхийг statement-д бичвэл хэмжилт "жагсаалт үүсгэх + хайх" хоёрын нийлбэрийг хэмжинэ, бид зөвхөн хайлтыг сонирхож байсан. Энэ бол хэмжилтийн хамгийн түгээмэл алдааны нэг: юу хэмжиж байгаагаа тодорхой мэдэхгүй байх.
Олон мөр код хэмжих, эсвэл үр дүнг кодоор боловсруулах шаардлагатай үед timeit-ийг Python-оос хэрэглэнэ:
python
import timeit
def best_ns(stmt, setup="pass", number=1_000_000):
times = timeit.repeat(stmt, setup, repeat=5, number=number)
return min(times) / number * 1e9
print(f"{best_ns('x = 2 ** 10'):.1f} ns")8.4 nstimeit.repeat() нь statement-ийг number удаа ажиллуулах цувааг repeat удаа давтаж, цуваа бүрийн нийт хугацааны жагсаалтыг буцаана. Бид хамгийн бага утгыг авч, number-д хуваагаад, тэрбумаар үржүүлж наносекунд болгоно. 1e9 бол Хичээл 6.2-ын адил тэрбум.
Яагаад дундаж биш, хамгийн бага утга вэ? Компьютер дээр бусад програмууд, үйлдлийн систем, хэрэглэгчийн хулганы хөдөлгөөн бүгд хэмжилтэд саатал нэмж болно, гэвч хэзээ ч хасахгүй. Тиймээс хамгийн бага хугацаа нь кодын жинхэнэ зардалд хамгийн ойр, саатлаас хамгийн бага бохирдсон утга. timeit-ийн албан ёсны баримт бичиг ч ижил шалтгаанаар хамгийн бага утгыг санал болгодог. Шударга тэмдэглэл: зарим нөхцөлд, жишээлбэл сүлжээний хүсэлтэд, хэлбэлзэл өөрөө чухал мэдээлэл, тэр үед медиан ба тархалтыг харах хэрэгтэй. Энэ бүлгийн тооцоололд хамгийн бага хангалттай.
Python хөгжүүлэгчдийн дунд хоёр хэв маяг бий. “Look before you leap” буюу "үсрэхээсээ өмнө хар": үйлдэл хийхээс өмнө шалгах, if key in d: value = d[key]. “Easier to ask forgiveness than permission” буюу "зөвшөөрөл асуухаас уучлал гуйх хялбар": шууд хийж, алдааг барих, try: value = d[key] ба except KeyError. Олон хүний зөн совин: “exception-ууд удаан, тиймээс if шалгалт үргэлж хурдан”. Шалгая.
measure_lookup.py файл:
python
import timeit
def best_ns(stmt, setup="pass", number=1_000_000):
times = timeit.repeat(stmt, setup, repeat=5, number=number)
return min(times) / number * 1e9
SETUP = "d = {'Бат': 95}"
check_hit = """
if 'Бат' in d:
x = d['Бат']
"""
try_hit = """
try:
x = d['Бат']
except KeyError:
pass
"""
check_miss = check_hit.replace("Бат", "Ану")
try_miss = try_hit.replace("Бат", "Ану")
print(f"Түлхүүр байгаа, if: {best_ns(check_hit, SETUP):6.1f} ns")
print(f"Түлхүүр байгаа, try: {best_ns(try_hit, SETUP):6.1f} ns")
print(f"Түлхүүр байхгүй, if: {best_ns(check_miss, SETUP):6.1f} ns")
print(f"Түлхүүр байхгүй, try: {best_ns(try_miss, SETUP):6.1f} ns")Гурван давхар хашилттай string олон мөр кодыг statement болгон дамжуулах хамгийн цэвэр арга. replace() нь хайх түлхүүрийг "Бат"-аас dict-д байхгүй "Ану" болгож, байхгүй тохиолдлын хувилбаруудыг үүсгэнэ. Ажиллуулна:
python measure_lookup.pyPython 3.13 бүхий нэг компьютер дээрх нэг ажиллуулалт:
Түлхүүр байгаа, if: 24.7 ns
Түлхүүр байгаа, try: 14.2 ns
Түлхүүр байхгүй, if: 13.9 ns
Түлхүүр байхгүй, try: 187.3 nsТаны тоонууд өөр байх нь хэвийн, гэвч харьцаанууд ойролцоо байх ёстой. Дүгнэлт хоёр чиглэлтэй бөгөөд хоёр нь хоёулаа зөн совингийн эсрэг.
Түлхүүр байх үед try илүү хурдан, энд 40 хувь орчим. if хувилбар dict-ийг хоёр удаа хайдаг: нэг удаа in-ээр, нэг удаа []-ээр. try хувилбар нэг удаа хайдаг. Python 3.11-ээс хойш алдаа гараагүй үед try блок бараг ямар ч зардалгүй болсон: тайлбарлагч try-д орохдоо нэмэлт ажил хийдэггүй. “try удаан” гэсэн зөн совин хуучин хувилбаруудаас үлдсэн, одоо буруу.
Түлхүүр байхгүй үед try арав дахин удаан. Exception-ийг шидэх нь бодит зардалтай: Python exception объект үүсгэж, traceback-ийн мэдээлэл бэлдэж, except-үүдийг шалгах ёстой. Тиймээс зөн совингийн тал нь зөв, гэхдээ зөвхөн exception бодитоор шидэгддэг тохиолдолд.
Практик дүгнэлт: зөв сонголт таны өгөгдлөөс хамаарна. Түлхүүр бараг үргэлж байдаг бол try илүү хурдан ба илүү цэвэр. Ихэвчлэн байдаггүй бол if эсвэл d.get(key). Энэ асуултад кодыг харж хариулах боломжгүй, зөвхөн таны бодит өгөгдлийн дээр хэмжиж. Хамгийн чухал нь: энэ ялгаа хэдэн арван наносекунд. Хэрэв таны програм энэ хайлтыг секундэд сая удаа хийдэггүй бол ялгаа хэзээ ч мэдрэгдэхгүй. Энэ тохиолдолд кодын тодорхой байдлаар сонго.
Хоёр дахь хэмжилт илүү гайхмаар. Хичээл 10.2-ын Stack төгсгөлөөс нь авдаг байсан: self._items.pop(). Дараалал буюу queue нь эсрэгээр, эхэлж ирсэн нь эхэлж гардаг: дэлгүүрийн касс дээрх оочир. Үүнийг list-ээр хийх хамгийн шууд арга нь items.pop(0): эхний элементийг авах. Энэ үйлдэл хэр хурдан вэ? Зөн совин: "эхний элементийг авах нь сүүлийнхийг авахтай адил энгийн".
Харьцуулахын тулд стандарт сангийн collections модулийн deque-ийг хэрэглэнэ: хоёр талаас нь нэмж, авах зориулалттай бүтэц. Энэ удаа нэг удаагийн том ажлыг хэмжих тул perf_counter хангалттай:
python
import time
from collections import deque
def drain_list(n):
items = list(range(n))
start = time.perf_counter()
while items:
items.pop(0)
return time.perf_counter() - start
def drain_deque(n):
items = deque(range(n))
start = time.perf_counter()
while items:
items.popleft()
return time.perf_counter() - start
for n in (25_000, 50_000, 100_000):
print(f"n={n:>7,}: list {drain_list(n):.4f} с, deque {drain_deque(n):.4f} с")Хэмжилтийн эхлэлийг жагсаалт үүсгэсний дараа тавьсан: timeit-ийн setup-тай ижил санаа. Нэг ажиллуулалт:
n= 25,000: list 0.0981 с, deque 0.0015 с
n= 50,000: list 0.4087 с, deque 0.0031 с
n=100,000: list 1.6412 с, deque 0.0063 сТаны тоонууд өөр байна, харин хэв маягийг хараарай. 100,000 элементэд list deque-ээс 250 гаруй дахин удаан. Гэвч хамгийн чухал мэдээлэл нь тоонуудын өөрчлөлтөд байна. n хоёр дахин өсөхөд deque-ийн хугацаа хоёр дахин өссөн: элемент бүрийг авах тогтмол зардалтай, нийт ажил элементийн тоотой пропорциональ. Харин list-ийн хугацаа дөрөв дахин өссөн: 0.10, 0.41, 1.64. Нийт ажил элементийн тооны квадрат-тай пропорциональ.
Яагаад? List дотроо элементүүдийг санах ойд дарааллаар нь хадгалдаг: 0-р байрлал, 1-р байрлал, гэх мэт. Эхний элементийг авахад тэр "нүх" үлдээж болохгүй тул бусад бүх элементийг нэг байрлалаар урагш шилжүүлнэ. 100,000 элементтэй жагсаалтад эхний pop 99,999 элементийг шилжүүлнэ, дараагийнх нь 99,998, гэх мэт. Нийт ойролцоогоор таван тэрбум шилжүүлэлт. Сүүлийн элементийг авах, pop(), ямар ч шилжүүлэлт шаарддаггүй тул хурдан. deque хоёр талдаа ч тогтмол зардалтайгаар нэмж, авахаар бүтээгдсэн. Код дээр pop(0) ба pop() хоёр бараг ижил харагддаг, гэвч зардлын хувьд огт өөр ертөнц.
Энэ хэмжилт хоёр сургамж өгнө. Нэгдүгээрт, үйлдлийн зардал нь кодын урт эсвэл "энгийн харагдах" байдлаас хамаардаггүй, өгөгдлийн бүтцийн дотоод зохион байгуулалтаас хамаардаг. Хоёрдугаарт, нэг хэмжээгээр хэмжих нь хангалтгүй. 1,000 элементтэй жагсаалтад pop(0) ердөө 0.0002 секунд зарцуулах тул ямар ч асуудалгүй харагдана. Асуудал зөвхөн өгөгдөл томрох үед илэрдэг. Тиймээс хэмжилтийн загварт хэмжээг өөрчилж хэмжих алхам заавал байна: хоёр дахин том өгөгдөл хугацааг хоёр дахин уу, дөрөв дахин уу нэмэгдүүлэх вэ? Энэ харьцаа кодын өсөлтийн зан чанарыг ямар ч онолгүйгээр харуулдаг. Хоёр дахин бол өсөлт шугаман, дөрөв дахин бол квадрат, хоёр дахин том өгөгдлөөр хугацаа бараг өөрчлөгдөхгүй бол тогтмол.
Компьютерийн шинжлэх ухааны алдартай эрдэмтэн Дональд Кнут 1974 оны нийтлэлдээ хамгийн их иш татагддаг нэгэн өгүүлбэр бичсэн: “premature optimization is the root of all evil”, "цаг бусын оновчлол бүх муугийн үндэс". Түүний бүрэн санаа илүү нарийн байсан: програмын цаг хугацааны ихэнх нь кодын маш жижиг хэсэгт зарцуулагддаг бөгөөд бусад хэсгийг оновчлох нь цаг алдах, кодыг эвдэх л болно. Гэвч тэр жижиг хэсгийг олж, оновчлох нь чухал. Асуудал нь тэр жижиг хэсэг хаана байгааг зөн совингоор олох боломжгүй. Тиймээс энэ бүлгийн хууль:
Хэмжихгүйгээр бүү оновчил.
Энэ хууль хэрэгжих загвар, энэ бүлгийн үлдсэн хичээлүүдэд бид үүнийг алхам бүрт дагана:
1. Таамаглал хэл. "Энэ функц удаан байх" гэж бичиж, ямар нотолгоо байгааг тэмдэглэ. Таамаглал бол буруу биш, тэр хэмжилтийн чиглэл.
2. Суурь хэмжилт хий. Өөрчлөхөөс өмнө одоогийн хугацааг бодит хэмжээний өгөгдөл дээр хэмж. Хэмжилт бүр setup-ийг тусгаарласан, олон давтсан, хамгийн бага утгаар байна.
3. Ганцхан зүйл өөрчил. Нэг дор хоёр өөрчлөлт хийвэл аль нь тусалсныг, аль нь хор хүргэсэнийг мэдэх боломжгүй.
4. Дахин хэмж. Ижил нөхцөлд, ижил өгөгдлөөр. Хэрэв хурд сайжраагүй бол өөрчлөлтийг буцаа: кодыг ойлгомжгүй болгосон ч ашиггүй оновчлол хадгалах үнэ цэнэгүй.
5. Хэмжээг өөрчилж шалга. Өгөгдлийг хоёр дахин томруулж, хугацаа хэрхэн өсч байгааг хар.
6. Шударга мэдээл. Тоог компьютер, Python-ий хувилбар, өгөгдлийн хэмжээтэй хамт бич. Тоонууд өөр компьютер дээр өөр байна гэдгийг хүлээн зөвшөөр, харьцааг онцол.
Хүсвэл measure_lookup.py-д d.get('Ану') хувилбар нэмж, түлхүүр байхгүй тохиолдолд if ба try-тэй харьцуулж үзээрэй. Таамаглалаа эхлээд бичээд, дараа нь хэмжээрэй: таамаглал хэр олон удаа таарах вэ? Сонирхвол Гайхшрал 2-ын drain_list-ийг pop(0)-ийн оронд урвуу дараалсан items.reverse() ба pop() болгож, хэмжилт хэрхэн өөрчлөгдөхийг харж болно. Мөн өөрийн Course 2 эсвэл Course 3-ын төслөөс "удаан байх" гэж сэжиглэж байсан нэг функц сонгоод, түүнийг хоёр өөр хэмжээний өгөгдөл дээр хэмжиж, өсөлт шугаман уу, квадрат уу гэдгийг тодорхойлох нь энэ хичээлийн загварыг бодит кодонд хэрэглэх анхны алхам. Эдгээр бүгд санал бөгөөд дараагийн хичээлүүд үүнийг хийсэн гэж үзэхгүй.
NameError: name ‘d’ is not defined. “d гэдэг нэр тодорхойлогдоогүй” гэсэн утгатай. timeit.repeat эсвэл timeit.timeit-ийн statement таны файлын хувьсагчийг ашиглахад гарна. timeit statement-ийг тусдаа, хоосон орчинд ажиллуулдаг: таны файлын хувьсагчид тэнд байхгүй. Хоёр шийдэл: хэрэгтэй бүх зүйлийг setup string-д бичих, эсвэл globals=globals() argument өгч таны модулийн нэрсийг timeit-д харуулах. Сүүлийнх нь өөрийн функцүүдийг хэмжихэд тохиромжтой: timeit.repeat("my_func(data)", globals=globals(), repeat=5, number=1000).
IndexError: pop from empty list. "Хоосон жагсаалтаас pop хийлээ" гэсэн утгатай. Төлөвийг өөрчилдөг statement-ийг timeit-ээр хэмжихэд гарна: python -m timeit -s "items = list(range(1000))" "items.pop(0)". Setup нэг удаа ажилладаг тул эхний 1,000 давталт жагсаалтыг хоосолж, 1,001 дэх давталт алдаа өгнө. Хэрэв жагсаалт хангалттай том байсан бол алдаа гарахгүй байх ч хэмжилт буруу байна: жагсаалт давталт бүрт богиносч, сүүлийн давталтууд эхнийхээс хамаагүй хурдан. Ийм үйлдлийг Гайхшрал 2-ын адил perf_counter-оор, эсвэл statement дотроо төлөвийг шинээр үүсгэж, тэр зардлыг тусад нь хэмжиж хасах аргаар хэмжинэ.
SyntaxError: unterminated string literal (detected at line 1). "Дуусаагүй string" гэсэн утгатай. Терминалаас timeit ажиллуулахад хашилтууд будлихад, ялангуяа Windows PowerShell-д, гарна. Гадна талын давхар хашилт ба доторх ганц хашилтыг хольж бичсэн байхад PowerShell заримдаа тэдгээрийг өөрөөр задалдаг. Хэмжилт хэдэн мөрөөс илүү эсвэл хашилт ихтэй бол measure_lookup.py шиг script файл бичих нь хамаагүй найдвартай.
Микро хэмжилтийг бүхэл програмд хэрэглэх. Мессежгүй, сэтгэлгээний алдаа. Гайхшрал 1-д try нь 10 наносекундээр хурдан байсан. Хэрэв програмын нийт ажиллах хугацаа 2 секунд бөгөөд энэ хайлт 100 удаа хийгддэг бол хэмнэлт нь нэг микросекунд: програмын хугацааны саяны нэгээс бага. Хэмжилт зөв, гэвч оновчлол утгагүй. timeit танд "энэ хэсэг хэр хурдан" гэж хэлдэг, харин "програмын хугацаа хаана зарцуулагддаг вэ" гэдгийг хэлдэггүй. Тэр асуултын хэрэгсэл дараагийн хичээлд.
Нэг удаагийн хэмжилтэд итгэх. Мессежгүй. perf_counter-оор нэг удаа хэмжсэн 0.003 секунд, ялангуяа маш богино ажилд, үйлдлийн системийн санамсаргүй саатлаар хоёр дахин хэлбэлзэж болно. Зөөврийн компьютер цахилгаан хэмнэлтийн горимд байхад процессорын хурд өөрчлөгдөж, бүх хэмжилтийг гажуудуулна. Хэмжилтийг хэд хэдэн удаа давтаж, хамгийн бага утгыг ав, харьцуулах хэмжилтүүдийг ижил нөхцөлд, нэг дор хий.
Хурдны зөн совин гайхмаар олон удаа буруу байдаг тул энэ бүлгийн хууль бол хэмжихгүйгээр бүү оновчил. timeit бол жижиг кодын хэсгийг хэмжих стандарт хэрэгсэл: терминалаас python -m timeit -s "setup" "statement", кодоос timeit.repeat(). Setup хэмжилтэд орохгүй, statement олон удаа давтагдаж, хамгийн бага утга нь саатлаас хамгийн бага бохирдсон тул түүнийг авна. Хоёр гайхшрал хэмжигдсэн. try/except нь алдаа гараагүй үед if шалгалтаас хурдан, учир нь dict-ийг ганц удаа хайдаг бөгөөд Python 3.11-ээс хойш try-д орох нь бараг үнэгүй. Харин exception бодитоор шидэгдэхэд арав дахин удаан: сонголт бодит өгөгдлөөс хамаарна. list-ийн pop(0) элемент бүрийг шилжүүлдэг тул нийт ажил квадратаар өсдөг: хоёр дахин том өгөгдөл дөрөв дахин удаан. deque-ийн popleft тогтмол зардалтай. Хэмжилтийн загвар: таамаглал, суурь хэмжилт, ганц өөрчлөлт, дахин хэмжих, хэмжээг өөрчилж шалгах, шударга мэдээлэх. Хэмжээг хоёр дахин нэмэхэд хугацааны харьцаа кодын өсөлтийг онолгүйгээр харуулдаг.
timeit сэжигтэй жижиг хэсгийг хэмжихэд маш сайн. Гэвч бодит програмд эхний асуулт нь "аль хэсэг сэжигтэй вэ" гэдэг. Мянган мөр кодтой програм 30 секунд ажилладаг бол тэр 30 секунд аль функцэд зарцуулагдаж байна вэ? Мөр бүрийг timeit-ээр хэмжих боломжгүй. Дараагийн хичээлд эмчийн "цусны шинжилгээ" буюу profiler-тэй танилцана: Python-ий стандарт cProfile програм ажиллах явцад функц бүрийн дуудлагын тоо, зарцуулсан хугацааг бүртгэж, хүснэгт болгон харуулдаг. Бид удаан script-ийг профайллаж, тэр хүснэгтийг багана бүрээр нь Монгол хэлээр уншиж, програмын хугацааны ихэнхийг эзэлдэг ганц халуун цэгийг олно.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.