2. Транзисторы, биты и логические вентили

В коридорах Bell Labs в Мюррей-Хилл всегда витала особая атмосфера. Именно здесь в декабре 1947 года наши коллеги Джон Бардин и Уолтер Браттейн (в группе, которой руководил Уильям Шокли) собрали первый в мире работающий — точечный — транзистор из кусочка германия, пластиковой полоски и двух золотых контактов; все трое разделили за это Нобелевскую премию 1956 года. Позже Шокли предложил плоскостной транзистор, ставший основой массовой электроники. Это неказистое устройство навсегда вытеснило громоздкие, раскаленные и постоянно перегорающие вакуумные радиолампы и открыло кремниевую эру.

Сегодня на кристалле размером с ноготь в вашем смартфоне или сервере упаковано более 15 миллиардов транзисторов. Когда мы пишем микросервисы на Go, мы привыкли мыслить сущностями высокого порядка: горутинами, HTTP-хэндлерами, JSON-структурами и каналами. Но процессор бесконечно далек от этих абстракций. На самом дне вычислительной машины нет ни чисел, ни букв — там существуют лишь электрические поля и законы физики твердого тела.

Чтобы понимать, как работают атомарные инструкции, почему битовые маски не имеют себе равных по скорости и за счет чего мьютекс в Go весит всего 8 байт, нам необходимо спуститься на этот фундаментальный этаж.


1. Транзистор: Микроскопический электронный кран

Подавляющее большинство современных микропроцессоров построено по технологии CMOS (Complementary Metal-Oxide-Semiconductor / КМОП) на базе полевых транзисторов типа MOSFET. «Комплементарная» означает, что используются транзисторы двух типов: NMOS (проводит ток при высоком напряжении на затворе) и PMOS (проводит при низком). Пара PMOS (подтягивает выход к питанию) + NMOS (подтягивает к земле) образует инвертор и позволяет схеме почти не потреблять ток в статике — он течёт в основном при переключении.

Для инженера-программиста полевой транзистор — это не квантовая головоломка, а миниатюрный, управляемый напряжением почти безынерционный переключатель (у затвора есть ёмкость, которую нужно перезарядить, — именно в ней причина задержек и тепла). У него есть три вывода:

  1. Исток (Source): Контакт, откуда электроны входят в полупроводник.
  2. Сток (Drain): Контакт, куда электроны стремятся уйти.
  3. Затвор (Gate): Тончайшая управляющая пластина, отделенная от кремния слоем диэлектрика (исторически — оксида кремния; в современных техпроцессах, примерно с 45 нм, — high-k диэлектрик с металлическим затвором).
flowchart TD
    classDef process  fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef success  fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
    classDef error    fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
    classDef warning  fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
    classDef entry    fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef aux      fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
    classDef system   fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
    classDef data     fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
    classDef network  fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
    classDef accent   fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    Gate["<b>Затвор (Gate)</b><br>[Управляющее напряжение]"]
    Dielectric["<b>Диэлектрик (Оксид)</b>"]

    subgraph Channel ["Канал для электронов"]
        direction LR
        Source["<b>Исток (Source)</b>"]:::process -->|"Поток электронов"| Drain["<b>Сток (Drain)</b>"]
    end

    Gate -->|"Управляющее поле"| Dielectric
    Dielectric -.-> Channel
  

Механика предельно наглядна:

  • Нет напряжения на затворе ($0\text{ В}$): Кремниевый канал между истоком и стоком не проводит ток. Электроны заперты. Цепь разорвана (логический «выключатель выключен»).
  • Подали напряжение на затвор ($+0.8\dots 1.1\text{ В}$): Электрическое поле притягивает носители заряда к границе оксида, формируя проводящий канал. Электроны свободно устремляются от истока к стоку. Цепь замкнута («выключатель включен»).

2. Биты: Абстракция над аналоговым напряжением

В реальном кремнии не существует идеальных платонических нулей и единиц. Напряжение на проводнике — непрерывная аналоговая величина, подверженная шумам, наводкам и падению напряжения на сопротивлении дорожек.

Чтобы построить надежную дискретную логику, инженеры договорились о жестких диапазонах уровней напряжения (TTL / LVCMOS логика):

DIAGRAM
+1.2V ──┬─────────────────────────────  Логическая «1» (HIGH)
        │ Гарантированная область единицы
+0.8V ──┴─────────────────────────────
        ░░ Запретная зона (Шум / Неопределенность) ░░
+0.4V ──┬─────────────────────────────
        │ Гарантированная область нуля
 0.0V ──┴─────────────────────────────  Логический «0» (LOW)

Бит — это абстрактная математическая модель, выражающая факт: находится ли напряжение на конкретном проводнике выше верхнего порога (1) или ниже нижнего (0). Зазор между ними (запретная зона) предотвращает ложные переключения от теплового шума.


3. Логические вентили (Logic Gates)

Один транзистор — это просто выключатель. Но если объединить несколько транзисторов в элементарную схему, они начинают вычислять булевы функции. Эти базовые кирпичики процессора называются логическими вентилями.

3.1. Вентиль NOT (Инвертор)

Принимает на вход 1 (высокий потенциал) и выдает на выходе 0 (низкий потенциал), и наоборот. В КМОП-схемотехнике инвертор собирается всего из двух комплементарных транзисторов (один n-канальный, другой p-канальный).

3.2. Вентили AND (И) и OR (ИЛИ)

  • AND: Выдает 1 на выходе только тогда, когда на входе $A$ И на входе $B$ присутствует 1. В кремнии это транзисторы, включенные последовательно.
  • OR: Выдает 1, если хотя бы на одном из входов ($A$ ИЛИ $B$) присутствует 1. В кремнии это транзисторы, соединенные параллельно.
flowchart LR
    classDef process  fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef success  fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
    classDef error    fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
    classDef warning  fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
    classDef entry    fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef aux      fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
    classDef system   fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
    classDef data     fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
    classDef network  fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
    classDef accent   fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    subgraph AND_Block["Вентиль AND (Логическое И)"]
        direction LR
        A1["Вход A: 1"]:::entry --> G_AND["AND &"]
        B1["Вход B: 1"] --> G_AND
        G_AND --> OUT1["Выход: 1"]
    end

    subgraph OR_Block["Вентиль OR (Логическое ИЛИ)"]
        direction LR
        A2["Вход A: 1"]:::entry --> G_OR["OR |"]
        B2["Вход B: 0"] --> G_OR
        G_OR --> OUT2["Выход: 1"]
    end
  

3.3. Вентиль XOR (Исключающее ИЛИ)

Вентиль XOR возвращает 1 тогда и только тогда, когда значения на входах различаются ($0 \oplus 1 = 1$, $1 \oplus 0 = 1$, но $1 \oplus 1 = 0$ и $0 \oplus 0 = 0$).

Для программиста XOR — поистине волшебный инструмент:

  1. Он лежит в основе двоичных сумматоров (сложение без переноса — это в точности XOR).
  2. На нем держатся алгоритмы симметричного шифрования (потоковые шифры вроде ChaCha20 и AES-CTR) и хэш-функции.
  3. Он позволяет мгновенно сравнивать, маскировать и инвертировать биты.

4. Mechanical Sympathy: Побитовые операции в Go

Зачем инженеру высоконагруженного бэкенда разбираться в вентилях? Дело в том, что побитовые операторы в языках программирования предоставляют прямой доступ к вентилям процессора без промежуточных накладных расходов.

Когда процессор выполняет арифметические операции вроде сложения a + b, умножения a * b или деления a / b, данные прокачиваются через каскады логических схем разной сложности: целочисленное сложение — это тоже примерно 1 такт латентности, умножение a * b — порядка 3 тактов, а целочисленное деление a / b — от 10 до 40 и более тактов (зависит от микроархитектуры и разрядности). Побитовые же инструкции (AND, OR, XOR, a ^ b) пролетают через вентили АЛУ всего за один такт CPU ($0.3\text{ нс}$).

Побитовые операторы Go: Особенности и отличия от C/C++

В Go базовый набор операторов выглядит знакомо, но имеет критически важное авторское отличие:

Оператор Go Название Назначение
& AND Побитовое И: оставляет бит равным 1, только если он равен 1 в обоих операндах
\| OR Побитовое ИЛИ: выставляет бит в 1, если он равен 1 хотя бы в одном операнде
^ XOR (бинарный) Исключающее ИЛИ: инвертирует бит по маске
^ NOT (унарный) Побитовое отрицание: инвертирует все биты операнда
&^ AND NOT Сброс бита (Bit Clear): уникальный оператор Go
<< Left Shift Сдвиг бит влево (эквивалентен умножению на $2^n$)
>> Right Shift Сдвиг бит вправо (для беззнаковых и неотрицательных чисел эквивалентен делению на $2^n$; для отрицательных знаковых округляет к $-\infty$, а деление — к нулю: -7 >> 1 == -4, но -7 / 2 == -3)

5. Практика системного инженера: Битовые маски (Bitmasks)

Представьте, что вы проектируете сервис авторизации с множеством привилегий пользователя: чтение, запись, удаление, модерация, аудит.

Если объявить эти флаги в обычной структуре:

Go
type UserPermissions struct {
    CanRead   bool // 1 байт + padding
    CanWrite  bool // 1 байт + padding
    CanDelete bool // 1 байт + padding
    IsAdmin   bool // 1 байт + padding
}

Каждый bool займет в памяти 1 байт; у bool выравнивание тоже 1 байт, поэтому вся структура из четырех bool занимает ровно 4 байта (паддинг понадобился бы лишь при смешении с более крупными полями). Но четыре отдельных поля нельзя изменить атомарно одной операцией, и в конкурентной среде придётся защищать структуру мьютексом.

Вместо этого системный подход упаковывает независимые флаги в одно машинное слово (до 64 флагов в uint64; в примере ниже для краткости использован uint8, то есть до 8 флагов), управляемое через ключевое слово iota:

Go
package main

import "fmt"

type Role uint8

const (
    RoleRead   Role = 1 << iota // 00000001 (1)
    RoleWrite                   // 00000010 (2)
    RoleDelete                  // 00000100 (4)
    RoleAdmin                   // 00001000 (8)
)

func main() {
    // 1. Комбинируем права через побитовое ИЛИ (OR)
    var myRole = RoleRead | RoleWrite // 00000011 (3)

    // 2. Проверяем наличие конкретного права через побитовое И (AND)
    if myRole&RoleWrite != 0 {
        fmt.Println("Доступ на запись разрешен")
    }

    // 3. Сбрасываем право на запись через AND NOT (&^)
    myRole = myRole &^ RoleWrite // 00000001 (остался только RoleRead)

    // 4. Переключаем флаг (Toggle) через XOR (^)
    myRole = myRole ^ RoleAdmin // Включили Admin:  00001001
    myRole = myRole ^ RoleAdmin // Выключили Admin: 00000001

    fmt.Printf("Итоговая битовая маска: %08b\n", myRole)
}

Упаковка в единое машинное слово дает колоссальное преимущество: состояние объекта можно изменять атомарно без блокировок (lock-free): через цикл на инструкции CAS (Compare-And-Swap) или — начиная с Go 1.23 — через atomic.Or / atomic.And. (Обратите внимание: сами atomic-операции в Go определены для типов от 32 бит, поэтому для атомарных флагов вместо uint8 берут uint32/uint64.)


6. Как устроен sync.Mutex в рантайме Go

Самый наглядный пример Mechanical Sympathy в исходниках самого языка Go — структура sync.Mutex (в современных версиях Go публичный sync.Mutex — тонкая обёртка, а реализация живёт в src/internal/sync/mutex.go; до Go 1.24 — прямо в src/sync/mutex.go).

Многие новички воображают мьютекс как сложную структуру со списками очередей и системными дескрипторами. В действительности весь мьютекс Go состоит всего из двух полей (sema — это не дескриптор, а ключ для рантайм-семафора, в котором паркуются ждущие горутины):

Go
type Mutex struct {
    state int32
    sema  uint32
}

Все внутреннее состояние блокировки упаковано в одно 32-битное число state:

DIAGRAM
 31                          3       2          1           0
┌─────────────────────────────┬───────────┬────────────┬─────────────┐
│ Количество ждущих горутин   │ Starving  │   Woken    │   Locked    │
│       (waitersCount)        │  (голод)  │(разбужена) │(заблокирован│
│           29 бит            │   1 бит   │   1 бит    │    1 бит    │
└─────────────────────────────┴───────────┴────────────┴─────────────┘

В исходниках Go это описано через побитовые константы:

Go
const (
    mutexLocked = 1 << iota // 1 (бит 0)
    mutexWoken              // 2 (бит 1)
    mutexStarving           // 4 (бит 2)
    mutexWaiterShift = iota // 3 (смещение для счетчика)
)

Захват мьютекса без конкуренции (Fast Path) — это одна-единственная атомарная операция:

Go
// Если мьютекс свободен (state == 0), переводим бит 0 в 1 одной атомарной операцией
// (без конкуренции — порядка десятка тактов CPU: LOCK-префикс дороже обычной записи)
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
    return
}

Если бы авторы Go использовали отдельные поля под каждый флаг, мьютекс стал бы заметно тяжелее (а их в программах бывают тысячи и миллионы). Упаковка в битовые маски делает мьютекс компактным — всего 8 байт.


7. Вопросы с собеседований


Резюме

  1. Транзисторы — элементарные кремниевые переключатели, управляющие потоком электронов с помощью напряжения на затворе.
  2. Биты — абстракция над физическими порогами напряжения, защищающая логику от аналогового шума.
  3. Логические вентили (NOT, AND, OR, NAND, NOR, XOR) — базовые булевы схемы. Вентиль NAND обладает функциональной полнотой, позволяя собрать целый компьютер из одного типа элементов.
  4. Побитовые операторы Go (&, |, ^, &^, <<, >>) — самый быстрый инструмент инженера: как правило, одна простая инструкция (латентность 1 такт) без аллокаций.
  5. Битовые маски экономят память кэш-линий и позволяют реализовывать lock-free структуры с атомарными операциями (CAS), как это сделано в быстром пути sync.Mutex (сам мьютекс при этом блокирующий: при конкуренции горутина паркуется на семафоре).

Мы научились управлять отдельными битами. Но как научить кремний складывать числа $2 + 2$? В следующей лекции мы объединим логические вентили в комбинационные схемы и построим первый процессорный сумматор: 3. Комбинационная логика. Учим кремний считать.