9.4 Memória és CPU – az erőforrás-pazarlás felismerése
Áttekintő
Sok junior fejlesztő úgy gondolja, hogy a teljesítményproblémák összetett, nehezen felismerhető rejtélyek. A valóság az, hogy a legtöbb erőforrás-pazarlás jól felismerhető mintakövetés: felesleges ciklusok, adatok felesleges memóriában tartása, objektumok helytelen létrehozása, blokkoló I/O műveletek. Ez az alfejezet megtanít felismerni ezeket a mintákat – még azelőtt, hogy azok production problémává válnának.
Amit tanulsz: Hogyan gondolkodj erőforrás-tudatosan kódírás közben – nem performance expert szinten, hanem junior-tól elvárt alapszinten.
Részletes tartalom
Mi az erőforrás-pazarlás?
Kódod futás közben két fő erőforrást fogyaszt:
- CPU: mennyit számol a processzor
- Memória (RAM): mennyi adatot tart a program a memóriában egyszerre
Az erőforrás-pazarlás nem azt jelenti, hogy a kód lassú vagy sok memóriát fogyaszt – hanem azt, hogy feleslegesen fogyaszt. A különbség kulcsfontosságú: egy nagy adat betöltése szükségből nem pazarlás; ugyanazt az adatot feleslegesen kétszer betölteni már igen.
Miért számít ez juniorként?
A legtöbb cég nem fog production-ban lebénuló rendszert elfogadni egy junior kódjától. De ha a kódod tele van apró pazarló mintákkal, az összeadódik: lassabb alkalmazás, magasabb szerverfoglalkoztatottság, drágább infrastruktúra. Valaki majd code review-ban megjegyzi – jobb, ha te látod meg előbb.
Felesleges ciklusok – az „O(n²) csapda"
Az egyik leggyakoribb CPU-pazarlás: beágyazott ciklus ott, ahol nem kellene.
Python példa – rossz:
users = get_all_users() # 1000 elem
admins = get_all_admins() # 50 elem
for user in users:
for admin in admins:
if user.id == admin.id:
user.is_admin = True
Ez 1000 × 50 = 50 000 összehasonlítás. Ha az adathalmaz nő, ez exponenciálisan romlik.
Python példa – jobb:
admin_ids = {admin.id for admin in get_all_admins()} # O(1) keresés set-ben
for user in users:
user.is_admin = user.id in admin_ids
Most 1000 + 50 lépés. Nem kellett algoritmuselméleti PhD – csak felismerni, hogy a belső ciklus helyett set-et érdemes használni.
JavaScript példa – rossz:
const result = [];
for (let i = 0; i < items.length; i++) {
for (let j = 0; j < categories.length; j++) {
if (items[i].categoryId === categories[j].id) {
result.push({ ...items[i], categoryName: categories[j].name });
}
}
}
JavaScript példa – jobb:
const categoryMap = Object.fromEntries(categories.map(c => [c.id, c.name]));
const result = items.map(item => ({
...item,
categoryName: categoryMap[item.categoryId]
}));
A felismerési jel: Ha látsz egy for ciklust egy másik for (vagy find, filter, includes) cikluson belül, és az adathalmaz növekedhet – gondolkodj el rajta.
Nagy adatstruktúrák memóriában tartása
A memória-pazarlás egyik klasszikus formája: betöltöd az összes adatot, pedig csak egy részét használod.
PHP példa – rossz:
$orders = Order::all(); // 100 000 rendelés betöltve RAM-ba
foreach ($orders as $order) {
if ($order->status === 'pending') {
sendReminderEmail($order);
}
}
PHP példa – jobb:
Order::where('status', 'pending')
->chunk(100, function ($orders) {
foreach ($orders as $order) {
sendReminderEmail($order);
}
});
A chunk() (és hasonló streaming mechanizmusok más nyelvekben) 100 rekordot tölt be egyszerre, feldolgozza, majd a következő 100-at. A memóriahasználat konstans marad, nem nő az adatmennyiséggel.
Python (fájl olvasás) – rossz:
with open('large_log.txt') as f:
lines = f.readlines() # 2 GB fájl RAM-ba
for line in lines:
process(line)
Python – jobb:
with open('large_log.txt') as f:
for line in f: # soronként olvassa, nem egyszerre
process(line)
A kérdés, amit feltegyél: „Tényleg szükségem van az összes adatra egyszerre, vagy feldolgozhatom részletekben?"
Objektum létrehozás innerloop-ban
Egy ritkábban emlegetett, de valós CPU- és memóriaterhelés: drága objektumok létrehozása cikluson belül, ahol azokat egyszer is létre lehetett volna hozni kívül.
JavaScript példa – rossz:
const items = getItems(); // 10 000 elem
for (const item of items) {
const formatter = new Intl.NumberFormat('hu-HU', { style: 'currency', currency: 'HUF' });
item.formattedPrice = formatter.format(item.price);
}
Intl.NumberFormat objektum létrehozása 10 000-szer, holott egy példány is elég lenne.
JavaScript példa – jobb:
const formatter = new Intl.NumberFormat('hu-HU', { style: 'currency', currency: 'HUF' });
for (const item of items) {
item.formattedPrice = formatter.format(item.price);
}
Python példa – rossz:
import re
for line in log_lines:
match = re.search(r'\d{4}-\d{2}-\d{2}', line) # regex fordítás minden iterációban
if match:
process(match.group())
Python példa – jobb:
import re
DATE_PATTERN = re.compile(r'\d{4}-\d{2}-\d{2}') # egyszer fordítja le
for line in log_lines:
match = DATE_PATTERN.search(line)
if match:
process(match.group())
A felismerési jel: Ha cikluson belül látsz new ClassName(...), regex fordítást, vagy bármit, ami „inicializálásnak" tűnik – nézd meg, hogy ki lehetne-e emelni a ciklus elé.
File I/O szinkronban (blocking I/O)
A fájl- és hálózati műveletek lassúak: a processzor várhat milliszekundumokig, amíg a lemez vagy a hálózat válaszol. Ha ezt szinkron, blokkoló módon csinálod egy ciklusban, az összeadódik.
Node.js példa – rossz:
const fs = require('fs');
const filenames = getFilenames(); // 500 fájl
for (const filename of filenames) {
const content = fs.readFileSync(filename); // blokkol, amíg a fájl betölt
process(content);
}
Node.js példa – jobb:
const fs = require('fs').promises;
const filenames = getFilenames();
const contents = await Promise.all(
filenames.map(filename => fs.readFile(filename))
);
contents.forEach(process);
A Promise.all párhuzamosan indítja el az összes olvasást – a teljes időt nem 500 × T, hanem közelítőleg T határozza meg (ahol T az egyes fájlok olvasási ideje).
Fontos: Ez nem mindig a helyes megközelítés. Ha 50 000 fájlt olvasnál egyszerre, az is problémás (erőforrás kimerülés). A valóságban limitált párhuzamosság (pl. 10-20 egyszerre) a legjobb – de ez már haladóbb téma.
A felismerési jel: Szinkron fájlolvasás/írás, szinkron HTTP kérés cikluson belül – azonnal gyanakodj.
Mikor érdemes streamelni adat helyett betölteni?
Betöltés (load all) megfelelő, ha:
- Az adat kis méretű (pár ezer rekord, néhány MB)
- Az összes adaton egyszerre kell műveletet végezni (pl. rendezés, összesítés)
- Egyszeri feldolgozásról van szó
Streamelés megfelelő, ha:
- Az adat nagy (tízezer+ rekord, nagy fájlok)
- Sorban, egyenként dolgozod fel
- A feldolgozás nem igényli az összes adat egyidejű jelenlétét
Példa: CSV export generálás. Ha 500 000 sort töltesz be RAM-ba, majd írod ki fájlba – pazarlás. Ha soronként olvasol az adatbázisból és soronként írsz a fájlba – streaming, konstans memória.
Garbage collection alapfogalom
A legtöbb modern nyelv (Python, JavaScript, PHP, Java, C#) automatikusan kezeli a memóriát: a garbage collector (GC) időnként átfésüli a memóriát, és törli azokat az objektumokat, amelyekre már semmi nem hivatkozik.
Mit kell tudni juniorként:
- A GC nem azonnali. Amikor egy objektumra már nincs referencia, az nem törlődik azonnal – a GC akkor fut, amikor szükségesnek ítéli.
- Felesleges referenciák tartása megakadályozza a GC-t. Ha egy nagy listát egy globális változóban tartasz, a GC nem tudja felszabadítani.
- A GC futása CPU-t fogyaszt. Ha nagyon sok objektumot hozol létre és dobsz el rövid idő alatt, a GC folyamatosan fut – ez lassulást okoz.
Praktikus következmény: Ne tarts nagy objektumokat feleslegesen memóriában. Ha egy változóra már nincs szükséged egy hosszú függvényen belül, és az nagy adatot tartalmaz – tedd null-ra (vagy Python-ban del), hogy a GC dolgozhass.
big_data = load_huge_dataset()
result = process(big_data)
del big_data # felszabadítja a referenciát, GC elveheti
save_result(result)
Ez nem mindig szükséges – csak akkor, ha tudod, hogy a memória nyomás valós probléma.
Életszerű példák
1. eset: „Miért lassul be az alkalmazás?"
Egy junior fejlesztő jelentési oldalt készít. Minden jelentésnél betölti az összes felhasználót, majd szűr JavaScript-ben:
// Backend endpoint
app.get('/report', async (req, res) => {
const allUsers = await db.query('SELECT * FROM users'); // 50 000 sor
const activeUsers = allUsers.filter(u => u.status === 'active');
res.json(activeUsers);
});
A probléma: 50 000 sort tölt be RAM-ba, JSON-ná alakítja, elküldi a hálózaton, majd a kliens szűri. Helyesebb:
app.get('/report', async (req, res) => {
const activeUsers = await db.query("SELECT * FROM users WHERE status = 'active'");
res.json(activeUsers);
});
Az adatbázis a szűrésre van optimalizálva – csinálja ő.
2. eset: „Kifogyott a memória production-ban"
Egy junior CSV importáló scriptet ír:
def import_csv(filename):
with open(filename) as f:
rows = f.readlines() # 2 GB fájl
for row in rows:
parse_and_save(row)
8 GB RAM-os szerveren 2 GB fájlnál már a többi folyamat is szenved. Streaming megoldással:
def import_csv(filename):
with open(filename) as f:
for row in f:
parse_and_save(row)
Memóriahasználat: néhány KB az egész futás alatt.
3. eset: „Miért nem skálázódik?"
A kód 100 felhasználónál jól működik. 10 000-nél lebénul. Miért?
foreach ($users as $user) {
$permissions = Permission::where('user_id', $user->id)->get();
if (in_array('admin', $permissions->pluck('name')->toArray())) {
$adminUsers[] = $user;
}
}
Ez N+1 query: minden userhez egy DB lekérdezés. 10 000 user = 10 001 DB kérés.
$users = User::with('permissions')->get();
foreach ($users as $user) {
if ($user->permissions->contains('name', 'admin')) {
$adminUsers[] = $user;
}
}
Most: 2 DB lekérdezés, bármennyi user.
Tesztfeladatok
1. Melyik kódrészlet okoz felesleges CPU-terhelést nagy adathalmaznál?
A)
product_ids = {p.id for p in products}
for order in orders:
if order.product_id in product_ids:
process(order)
B)
for order in orders:
for product in products:
if order.product_id == product.id:
process(order)
C) Mindkettő ugyanolyan hatékony
D) A) változat lassabb, mert set-et épít
2. Egy alkalmazás 500 000 rekordot tölt be adatbázisból, majd soronként menti CSV-be. Mi a probléma és mi a megoldás?
A) Nincs probléma, ez a helyes megközelítés
B) Felesleges memóriahasználat; megoldás: streaming / chunk-alapú feldolgozás
C) Az adatbázis-lekérdezés lassú; megoldás: index hozzáadása
D) A CSV írás lassú; megoldás: párhuzamos írás
3. Mit tesz a garbage collector?
A) Minden felesleges CPU-ciklust leállít
B) Automatikusan törli a már nem hivatkozott objektumokat a memóriából
C) Optimalizálja az adatbázis lekérdezéseket
D) Tömöríti a fájlokat
4. Melyik módosítás javítja a következő kód teljesítményét?
for (const item of largeList) {
const pattern = new RegExp('^\\d{4}-\\d{2}-\\d{2}$');
if (pattern.test(item.date)) {
validItems.push(item);
}
}
A) A new RegExp() kiemelése a ciklus elé
B) A largeList kicserélése tömbre
C) A push() helyett concat() használata
D) A ciklus while-ra cserélése
5. Mikor érdemes streamelni adat helyett egyszerre betölteni?
A) Mindig streamelni kell, az mindig jobb
B) Mindig egyszerre kell betölteni, az egyszerűbb
C) Ha az adat nagy és sorban, egyenként feldolgozható – streamelés; ha kis adat és egyszerre kell – betöltés
D) Csak adatbázis-műveleteknél érdemes streamelni
6. Melyik szituációban NEM gyanakodj erőforrás-pazarlásra?
A) 1000 elemű listában minden elemhez meghívod a belső ciklust, ami szintén 1000 elemet iterál
B) Egy 100 KB-os JSON fájlt betöltesz memóriába és parse-olsz
C) 2 GB-os log fájlt readlines()-szal töltesz be RAM-ba
D) Egy objektumot (pl. regex pattern) belső cikluson belül hozol létre 50 000-szer
7. Mi a helyes megközelítés szinkron fájlolvasásnál cikluson belül (Node.js)?
A) fs.readFileSync() cikluson belül – ez a legbiztonságosabb
B) Párhuzamos aszinkron olvasás Promise.all()-lal – gyorsabb, de limitált párhuzamosság esetén biztonságos
C) Fájlonként külön processzt indítani
D) Ciklus nélkül, egyszerre beolvasni az összes fájlt fs.readFileSync()-kel
8. Miért problémás ez a PHP kód?
foreach ($products as $product) {
$category = Category::find($product->category_id);
echo $category->name;
}
A) A foreach PHP-ban lassú, for ciklust kell használni
B) N+1 query probléma: minden termékhez külön DB lekérdezés fut
C) A Category::find() nem biztonságos
D) Nincs probléma, ez helyes kód
9. Melyik állítás igaz a garbage collection-ről?
A) A GC azonnali: amint egy objektumra nincs referencia, azonnal felszabadítja
B) A GC futása nem fogyaszt CPU-t
C) A GC nem tud felszabadítani olyan objektumokat, amelyekre még van referencia – még ha felesleges is az
D) A GC csak Java-ban létezik, Python-ban nincs
10. Hogyan ismered fel, hogy egy cikluson belüli objektumlétrehozás problémás?
A) Ha az objektum neve nagybetűvel kezdődik
B) Ha a new ClassName() vagy hasonló inicializáció ciklus törzsében van, és az objektum minden iterációban ugyanolyan konfigurációval jön létre
C) Ha a ciklus 10-nél több iterációt fut
D) Ha az objektum adatbázisból olvas