Модуль 36

Оптимізація памʼяті та продуктивності

Ось урок, створений у стилі CS50: енергійний, з аналогіями та акцентом на "чому", а не просто "як".


🎓 CS50: Оптимізація памʼяті та продуктивності

Привіт, друзі! Радий бачити вас.

1. 🔥 Вступ: Чому ваш код "гальмує"?

Уявіть ситуацію. Ви написали скрипт, який обробляє замовлення в інтернет-магазині. Ви протестували його на 10 замовленнях — усе літає! Ви, задоволені, заливаєте код на сервер.

Настає "Чорна п'ятниця". Приходить 100 000 замовлень. І раптом... тиша. Сервер завис. Користувачі бачать "біле колесо смерті". Ваш менеджер телефонує вам у паніці.

Питання до вас: Чому код, який ідеально працював з 10 елементами, "помер" на 100 000?

Це як із переїздом. Якщо у вас одна коробка речей, ви можете перенести її в руках до машини за хвилину. А якщо у вас 1000 коробок? Ви не можете просто бігати туди-сюди 1000 разів — це займе весь день (це проблема часу/продуктивності). А якщо ви спробуєте взяти всі 1000 коробок одночасно, ви просто впадете під їхньою вагою (це проблема пам'яті).

Сьогодні ми навчимося писати код, який не "падає" під навантаженням. Ми поговоримо про ефективність.


2. 🧠 Теоретична база: Час vs Простір

В інформатиці є два головних ресурси, за які ми постійно платимо: 1. CPU (Час): Як швидко процесор виконає інструкції. 2. RAM (Пам'ять): Скільки місця потрібно, щоб зберігати дані під час роботи.

Що відбувається "під капотом"?

Уявіть, що Оперативна пам'ять (RAM) — це ваш робочий стіл. * Кожна змінна, яку ви створюєте (x = [1, 2, 3...]), займає місце на столі. * Якщо стіл завалений, комп'ютер починає використовувати Swap (скидати дані на жорсткий диск/SSD). Це як класти документи на підлогу в іншій кімнаті. Щоб їх дістати, треба встати і йти туди. Це дуже повільно.

Big O (Велике О) — без паніки!

Це не вища математика, це просто спосіб сказати: "Як зміниться час роботи, якщо даних стане більше?"

  • O(1) — Constant Time: Це ідеал. Ви знаєте, де лежить книга, і берете її. Час однаковий, чи у вас 1 книга, чи мільйон. (Наприклад, доступ до елемента масиву за індексом).
  • O(n) — Linear Time: Читання книги сторінка за сторінкою. Якщо сторінок удвічі більше, читатимете вдвічі довше. (Звичайний цикл for).
  • O(n²) — Quadratic Time: Жах розробника. На кожен елемент ви переглядаєте всі інші елементи. (Вкладені цикли). Якщо даних стало в 10 разів більше, програма працюватиме в 100 разів повільніше!

Запам'ятайте головне: Ми завжди шукаємо компроміс (Trade-off). Часто, щоб зробити код швидшим (Time), нам треба використати більше пам'яті (Space). І навпаки.


3. 🧪 Приклади: Від "равлика" до "ракети"

Давайте подивимось на Python, але логіка працює скрізь (C, Java, JS).

Приклад 1: "Наївна" конкатенація рядків

Уявіть, що нам треба склеїти 100 000 слів у один текст.

Як ви думаєте, що буде, якщо ми зробимо так?

# Спосіб 1: Поганий
text = ""
words = ["hello"] * 100000  # Список зі 100 тисяч слів

for word in words:
    text = text + word + " "

Інтуїція підказує: "Ну, ми просто додаємо слова, це має бути швидко". Реальність: Це O(n²). Чому? Тому що рядки в багатьох мовах незмінні (immutable). Кожен + створює НОВИЙ рядок у пам'яті, копіюючи старий вміст + нове слово. Це як будувати стіну: поклали цеглу, розібрали всю стіну, побудували нову на одну цеглу більшу.

Оптимізований варіант:

# Спосіб 2: Хороший (Pythonic way)
words = ["hello"] * 100000
text = " ".join(words)

Чому це краще? Ми заздалегідь знаємо розмір усіх слів, виділяємо пам'ять один раз і просто "заливаємо" туди дані. Це O(n). Різниця в швидкості може бути в тисячі разів.


Приклад 2: Генератори (Економія пам'яті)

У вас є файл розміром 10 ГБ. Вам треба порахувати кількість рядків.

Варіант А (Завантажити все в список):

def read_file_bad():
    # Це завантажить УВЕСЬ файл у RAM. 
    # Якщо у вас 8 ГБ RAM, а файл 10 ГБ — комп'ютер "повіситься".
    lines = open("huge_data.txt").readlines() 
    return len(lines)

Варіант Б (Ліниве читання):

def read_file_good():
    count = 0
    # Ми читаємо по одному рядку за раз.
    # Пам'ять використовується тільки для ОДНОГО рядка!
    for line in open("huge_data.txt"): 
        count += 1
    return count

Результат: Варіант Б працює навіть на слабкому ноутбуці з файлами будь-якого розміру.


4. 🛠 Практична частина

Час забруднити руки! Відкривайте IDE.

Завдання 1: Відчуй різницю Напиши код з Прикладу 1 (склеювання рядків). Заміряй час виконання для 10,000 слів і для 100,000 слів (використовуй модуль time). * Питання: Чи зріс час лінійно (в 10 разів) чи набагато більше?

Завдання 2: Пошук у списку vs Set Створи список my_list із 10 мільйонів чисел. Створи my_set із тих самих чисел. Спробуй знайти число 9999999 у списку (if x in my_list) і в сеті (if x in my_set). * Очікування: Пошук у list — це O(n) (довго). Пошук у set — це O(1) (миттєво, бо це хеш-таблиця). Перевір це!

Завдання 3: Міні-кейс Вам приходять дані про користувачів (список ID), але там багато дублікатів. Вам треба повернути список тільки унікальних ID. * Напиши функцію unique_ids(ids), яка робить це максимально швидко. (Підказка: не використовуй цикл у циклі).

Завдання 4: "А що, якщо..." А що, якщо нам треба не просто порахувати рядки у файлі 100 ГБ, а відсортувати їх? Ми не можемо завантажити їх у пам'ять. * Подумай: Як би ти це зробив? (Гуглити термін "External Merge Sort" дозволено, але спробуй спочатку придумати логіку: може, розбити файл на шматки?).


5. 💡 Мислення як у розробника

Як думає досвідчений інженер (Senior), коли бачить задачу?

  1. НЕ оптимізує передчасно. > "Premature optimization is the root of all evil" (Donald Knuth). Спочатку напиши код, який працює правильно і читабельно. Оптимізуй тільки тоді, коли бачиш реальну проблему або знаєш, що даних буде багато.

  2. Думає про масштабування. Він завжди запитує: "А що буде, якщо в цей масив прилетить мільйон записів?". Якщо відповідь "Сервер впаде", він змінює алгоритм.

  3. Використовує Профайлери. Ми не гадаємо на кавовій гущі. Ми запускаємо інструмент (Profiler), який показує: "Ось цей рядок коду займає 90% часу". І правимо тільки його.

  4. Типові помилки новачків:

    • Зчитування всього файлу в пам'ять read().
    • Використання списків (list) там, де потрібен пошук (set або dict).
    • Створення зайвих копій даних (Variables).

6. 🧩 Підсумок

Отже, що ми сьогодні винесли з лекції?

  1. Ресурси обмежені. RAM — це не бездонна бочка.
  2. Алгоритм важливіший за залізо. Поганий код (O(n²)) "покладе" навіть суперкомп'ютер. Хороший код (O(n) або O(log n)) літатиме на старому ноутбуці.
  3. Інструменти: Ви знаєте різницю між list і generator, і чому join краще за +.

Тепер ви вмієте: Дивитися на код не просто як на текст, а як на механізм, що споживає ресурси. Ви перейшли від "аби працювало" до "працює ефективно".

🚀 На наступному уроці: Ми дізнаємося, як зберігати дані так, щоб знаходити їх миттєво, не перебираючи все підряд. Готуйтеся, ми зануримось у Структури даних та Хеш-таблиці!

Це був CS50. (Кидає мікрофон... віртуально). Кодуйте з розумом!