Архитектура компьютера¶
Архитектура компьютера — это описание структуры и принципов работы вычислительной системы. Понимание того, как работает железо "под капотом", делает вас лучшим программистом: вы пишете более эффективный код, понимаете узкие места и можете оптимизировать производительность.
1. Основные компоненты компьютера 🖥️¶
Процессор (CPU — Central Processing Unit)¶
"Мозг" компьютера, выполняющий инструкции программы.
Структура CPU:
┌─────────────────────────────────────────────────────────────────────┐
│ Компонент │ Назначение │ Пример │
├───────────────┼──────────────────────────────────────┼─────────────┤
│ АЛУ (ALU) │ Арифметико-логические операции: │ 5 + 3 = 8 │
│ │ сложение, вычитание, сравнение │ x > y │
├───────────────┼──────────────────────────────────────┼─────────────┤
│ УУ (CU) │ Декодирование инструкций, управление │ Чтение │
│ │ потоком данных │ команды из │
│ │ │ памяти │
├───────────────┼──────────────────────────────────────┼─────────────┤
│ Регистры │ Сверхбыстрая память внутри CPU для │ EAX, EBX │
│ │ временного хранения данных │ (x86) │
├───────────────┼──────────────────────────────────────┼─────────────┤
│ Кэш L1/L2/L3 │ Быстрая буферная память между CPU │ L1: 32-64 │
│ │ и RAM │ KB на ядро │
└───────────────┴──────────────────────────────────────┴─────────────┘
Тактовая частота: Измеряется в ГГц (миллиардах циклов в секунду). За один такт может выполняться одна или несколько микроопераций.
Важно: Больше ГГц ≠ всегда быстрее. Архитектура (IPC — instructions per cycle) важнее чистой частоты.
Память (Memory Hierarchy) 📊¶
Иерархия памяти строится по принципу "быстрее = меньше = дороже":
┌─────────────────────────────────────┐
│ Регистры CPU │ ← 1 цикл, ~1 байт
│ (самая быстрая) │
├─────────────────────────────────────┤
│ Кэш L1 (на ядро) │ ← 3-4 цикла, ~32-64 KB
├─────────────────────────────────────┤
│ Кэш L2 (на ядро) │ ← 10-12 циклов, ~256 KB
├─────────────────────────────────────┤
│ Кэш L3 (общий) │ ← 30-40 циклов, ~8-32 MB
├─────────────────────────────────────┤
│ Оперативная память (RAM) │ ← 100+ циклов, ~8-64 GB
├─────────────────────────────────────┤
│ SSD / NVMe (вторичная память) │ ← 100,000+ циклов, ~256 GB - 4 TB
├─────────────────────────────────────┤
│ HDD (медленная вторичная) │ ← 1,000,000+ циклов, ~1-10 TB
└─────────────────────────────────────┘
Типы памяти: - RAM (Random Access Memory): Энергозависимая, быстрая, для работающих программ. - DRAM: Дешевле, медленнее, используется для основной памяти. - SRAM: Быстрее, дороже, используется для кэша. - ROM (Read-Only Memory): Энергонезависимая, хранит прошивки (BIOS/UEFI). - Flash (SSD, USB): Энергонезависимая, для долговременного хранения.
Ввод-вывод (I/O) 🔌¶
Способ взаимодействия CPU с периферийными устройствами (клавиатура, диск, сеть).
Механизмы I/O: 1. Программируемый I/O: CPU активно опрашивает устройство (неэффективно). 2. Прерывания (Interrupts): Устройство сигнализирует CPU о готовности данных. CPU приостанавливает текущую задачу, обрабатывает прерывание, возвращается обратно. 3. DMA (Direct Memory Access): Специальный контроллер передает данные между устройством и памятью без участия CPU. CPU только инициирует передачу и получает уведомление о завершении.
Пример: Загрузка файла с диска в память. DMA освобождает CPU для других задач.
2. Принципы работы процессора ⚙️¶
Фон-неймановская архитектура¶
Классическая архитектура, предложенная Джоном фон Нейманом (1945):
Основные принципы: 1. Единая память: Программы и данные хранятся в одной памяти. 2. Последовательное выполнение: Команды выполняются одна за другой. 3. Адресуемость памяти: Каждая ячейка памяти имеет уникальный адрес. 4. Условные переходы: Возможность изменять порядок выполнения команд (if, loops).
Цикл выполнения команды (Fetch-Decode-Execute):
1. Fetch (Выборка): Получить команду из памяти по адресу PC (Program Counter)
2. Decode (Декодирование): Определить, что нужно сделать
3. Execute (Выполнение): Выполнить операцию (АЛУ, запись в память, переход)
4. Update PC: Увеличить счетчик команд (или установить новый адрес при переходе)
Проблема фон-неймановского бутылочного горлышка: Пропускная способность шины между CPU и памятью ограничивает скорость работы. Решение → кэш-память.
Конвейеризация (Pipelining) 🏭¶
Техника ускорения выполнения команд за счет параллелизма на уровне стадий.
Идея: Разбить выполнение команды на стадии и выполнять разные команды на разных стадиях одновременно.
Классический 5-ступенчатый конвейер (MIPS):
Время →
Команда 1: IF → ID → EX → MEM → WB
Команда 2: IF → ID → EX → MEM → WB
Команда 3: IF → ID → EX → MEM → WB
Команда 4: IF → ID → EX → MEM → WB
Преимущество: В идеале — 1 команда за такт (vs 5 тактов на команду без конвейера).
Проблемы конвейера: - Конфликты данных: Команда зависит от результата предыдущей, которая ещё не завершилась. - Решение: Forwarding (пересылка результатов напрямую), stall (остановка конвейера). - Конфликты управления: Условный переход (if/else) — неизвестно, какую команду грузить дальше. - Решение: Предсказание переходов (branch prediction), спекулятивное выполнение.
Многозадачность и многопоточность 🔄¶
Многозадачность (Multitasking): Быстрое переключение между задачами (процессами), создающее иллюзию параллельной работы.
Переключение контекста: 1. Сохранить состояние текущего процесса (регистры, PC, стек) в PCB (Process Control Block). 2. Загрузить состояние следующего процесса из его PCB. 3. Продолжить выполнение.
Цена переключения: ~1-10 мкс. Частые переключения снижают производительность.
Многопоточность (Multithreading): - Аппаратная (Hyper-Threading): Одно физическое ядро выполняет несколько потоков, переключаясь между ними при ожиданиях (например, доступ к памяти). - Программная: ОС планирует потоки одного или разных процессов на разных ядрах.
3. Представление данных 💾¶
Числа в компьютере¶
Двоичная система (base-2):
- Основание: 2 (цифры 0, 1).
- Пример: 1011₂ = 1×8 + 0×4 + 1×2 + 1×1 = 11₁₀
Шестнадцатеричная система (base-16):
- Основание: 16 (цифры 0-9, A-F).
- Удобна для записи байтов: FF₁₆ = 255₁₀ = 11111111₂.
- Пример адреса памяти: 0x7FFF_FFFF.
Целые числа:
- Без знака (unsigned): Все биты — значение. Диапазон для 8 бит: 0..255.
- Со знаком (signed): Старший бит — знак (0=+, 1=-), остальные — значение в дополнительном коде (Two's Complement).
- Дополнительный код: инвертировать все биты и прибавить 1.
- Пример (-5 в 8 битах): 5 = 00000101 → инверсия 11111010 → +1 = 11111011.
- Диапазон для 8 бит: -128..127.
Числа с плавающей точкой (IEEE 754):
Формат для вещественных чисел: (-1)^sign × mantissa × 2^(exponent-bias)
┌─────────────┬──────────────┬──────────────┬──────────────────────┐
│ Тип │ Биты │ Диапазон │ Точность │
├─────────────┼──────────────┼──────────────┼──────────────────────┤
│ float32 │ 32 (1+8+23) │ ±10^±38 │ ~7 десятичных │
│ │ │ │ цифр │
├─────────────┼──────────────┼──────────────┼──────────────────────┤
│ float64 │ 64 (1+11+52)│ ±10^±308 │ ~15 десятичных │
│ │ │ │ цифр │
└─────────────┴──────────────┴──────────────┴──────────────────────┘
Важно:
0.1 + 0.2 ≠ 0.3в float из-за неточности представления. Не используйте float для финансовых расчетов!
Кодировки текста¶
┌──────────────────────┬─────────────────────────────────────┬──────────────────┐
│ Кодировка │ Описание │ Размер символа │
├──────────────────────┼─────────────────────────────────────┼──────────────────┤
│ ASCII │ 7 бит, 128 символов (латиница, │ 1 байт │
│ │ цифры, управляющие) │ │
├──────────────────────┼─────────────────────────────────────┼──────────────────┤
│ Latin-1 │ 8 бит, 256 символов (ASCII + │ 1 байт │
│ (ISO-8859-1) │ европейские буквы) │ │
├──────────────────────┼─────────────────────────────────────┼──────────────────┤
│ UTF-8 │ Переменная длина (1-4 байта), │ 1-4 байта │
│ │ обратно совместима с ASCII │ │
├──────────────────────┼─────────────────────────────────────┼──────────────────┤
│ UTF-16 │ 2 или 4 байта на символ, │ 2-4 байта │
│ │ используется в Windows/Java │ │
└──────────────────────┴─────────────────────────────────────┴──────────────────┘
UTF-8 (стандарт интернета):
- ASCII (0-127): 1 байт (0xxxxxxx).
- Кириллица (128-2047): 2 байта (110xxxxx 10xxxxxx).
- Китайские иероглифы: 3 байта.
- Эмодзи: 4 байта.
Проблема: "Кракозябры" возникают при неправильном определении кодировки. Всегда явно указывайте
encoding='utf-8'при работе с файлами.
4. Оптимизация кода с учетом архитектуры 🚀¶
Локальность данных¶
Временная локальность: Если к данным обратились сейчас, скорее всего, обратятся снова скоро. - Совет: Повторно используйте переменные, держите "горячие" данные в кэше.
Пространственная локальность: Если обратились к данным, скорее всего, обратятся к соседним. - Совет: Обрабатывайте массивы последовательно, используйте структуры вместо массивов структур (SoA vs AoS).
Пример (плохо vs хорошо):
# ❌ Плохо: скачкообразный доступ к памяти (промах кэша)
for i in range(0, 1000000, 1000):
process(array[i])
# ✅ Хорошо: последовательный доступ (попадание в кэш)
for i in range(1000000):
process(array[i])
Выравнивание данных (Alignment)¶
CPU обращается к памяти блоками (обычно 4 или 8 байт). Если данные не выровнены по границе блока, требуется 2 обращения вместо 1.
// ❌ Плохо: структура 9 байт, выравнивание нарушено
struct Bad {
char a; // 1 байт
int b; // 4 байта (смещение 1, нужно 3 байта padding)
short c; // 2 байта
}; // Итого: 1 + 3(padding) + 4 + 2 = 10 байт (не 7!)
// ✅ Хорошо: явное выравнивание
struct Good {
int b; // 4 байта
short c; // 2 байта
char a; // 1 байт
// 1 байт padding до границы 4
}; // Итого: 8 байт
Branch Prediction (Предсказание переходов)¶
CPU предсказывает, куда пойдет ветвление (if/else), и начинает выполнять команды заранее. Если предсказание неверно — конвейер сбрасывается (~10-20 тактов потерь).
Совет: Избегайте непредсказуемых условий в горячих циклах.
# ❌ Плохо: случайные данные, предсказатель ошибается в 50% случаев
for x in random_array:
if x > 128:
sum += x
# ✅ Хорошо: отсортированные данные, предсказатель работает идеально
for x in sorted_array:
if x > 128:
sum += x
📝 Шпаргалка: Порядок величин¶
┌──────────────────────┬──────────────┬───────────────────────────────────┐
│ Операция │ Время │ Эквивалент в циклах CPU (3 ГГц) │
├──────────────────────┼──────────────┼───────────────────────────────────┤
│ Такт CPU │ 0.3 нс │ 1 цикл │
├──────────────────────┼──────────────┼───────────────────────────────────┤
│ Доступ к кэшу L1 │ 1 нс │ ~3 цикла │
├──────────────────────┼──────────────┼───────────────────────────────────┤
│ Доступ к кэшу L2 │ 4 нс │ ~12 циклов │
├──────────────────────┼──────────────┼───────────────────────────────────┤
│ Доступ к кэшу L3 │ 15 нс │ ~45 циклов │
├──────────────────────┼──────────────┼───────────────────────────────────┤
│ Доступ к RAM │ 100 нс │ ~300 циклов │
├──────────────────────┼──────────────┼───────────────────────────────────┤
│ Чтение SSD (NVMe) │ 100 мкс │ ~300,000 циклов │
├──────────────────────┼──────────────┼───────────────────────────────────┤
│ Чтение HDD │ 10 мс │ ~30,000,000 циклов │
├──────────────────────┼──────────────┼───────────────────────────────────┤
│ Сетевой запрос │ 1-100 мс │ ~3M - 300M циклов │
│ (RTT) │ │ │
└──────────────────────┴──────────────┴───────────────────────────────────┘
Вывод: Один промах кэша — это сотни потерянных циклов. Одна операция с диском — миллионы циклов. Оптимизируйте доступ к памяти!
🎯 Практическое применение в разработке¶
- Базы данных: B-деревья спроектированы с учетом размера страницы диска (4 KB) и кэш-линий CPU.
- Игры: Data-oriented design для максимальной утилизации кэша и SIMD-инструкций.
- Веб-серверы: Асинхронный I/O (epoll, kqueue) вместо потоков для обработки тысяч соединений без переключения контекста.
- Big Data: Columnar storage (Parquet, ORC) для лучшей компрессии и локальности при аналитических запросах.
- Криптография: Защита от timing attacks через постоянное время выполнения операций.
Запомните: Хороший программист знает синтаксис языка. Отличный программист понимает, как его код выполняется на железе.