Перейти к содержанию

Архитектура компьютера

Архитектура компьютера — это описание структуры и принципов работы вычислительной системы. Понимание того, как работает железо "под капотом", делает вас лучшим программистом: вы пишете более эффективный код, понимаете узкие места и можете оптимизировать производительность.


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
- IF (Instruction Fetch): Выборка команды - ID (Instruction Decode): Декодирование и чтение регистров - EX (Execute): Выполнение операции - MEM (Memory Access): Доступ к памяти - WB (Write Back): Запись результата в регистр

Преимущество: В идеале — 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)               │              │                                   │
└──────────────────────┴──────────────┴───────────────────────────────────┘

Вывод: Один промах кэша — это сотни потерянных циклов. Одна операция с диском — миллионы циклов. Оптимизируйте доступ к памяти!


🎯 Практическое применение в разработке

  1. Базы данных: B-деревья спроектированы с учетом размера страницы диска (4 KB) и кэш-линий CPU.
  2. Игры: Data-oriented design для максимальной утилизации кэша и SIMD-инструкций.
  3. Веб-серверы: Асинхронный I/O (epoll, kqueue) вместо потоков для обработки тысяч соединений без переключения контекста.
  4. Big Data: Columnar storage (Parquet, ORC) для лучшей компрессии и локальности при аналитических запросах.
  5. Криптография: Защита от timing attacks через постоянное время выполнения операций.

Запомните: Хороший программист знает синтаксис языка. Отличный программист понимает, как его код выполняется на железе.