惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tenable Blog
月光博客
月光博客
雷峰网
雷峰网
WordPress大学
WordPress大学
博客园 - 司徒正美
Last Week in AI
Last Week in AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
Visual Studio Blog
H
Help Net Security
Engineering at Meta
Engineering at Meta
Google DeepMind News
Google DeepMind News
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
S
Security @ Cisco Blogs
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
爱范儿
爱范儿
W
WeLiveSecurity
J
Java Code Geeks
Forbes - Security
Forbes - Security
H
Hacker News: Front Page
T
Threatpost
The Cloudflare Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
N
Netflix TechBlog - Medium
Latest news
Latest news
V2EX - 技术
V2EX - 技术
小众软件
小众软件
T
The Blog of Author Tim Ferriss
A
Arctic Wolf
B
Blog RSS Feed
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
I
InfoQ
C
Check Point Blog
N
News | PayPal Newsroom
Cyberwarzone
Cyberwarzone
V
V2EX
TaoSecurity Blog
TaoSecurity Blog
P
Privacy & Cybersecurity Law Blog
Microsoft Security Blog
Microsoft Security Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
D
DataBreaches.Net
F
Fortinet All Blogs
阮一峰的网络日志
阮一峰的网络日志
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
IT之家
IT之家
K
Kaspersky official blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Google DeepMind News
Google DeepMind News
C
CXSECURITY Database RSS Feed - CXSecurity.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Что именно делал компилятор: как ассемблер помогает разобраться в производительности кода на C++
Sivchenko_translate · 2026-06-01 · via Все публикации подряд на Хабре

10 мин

15K

Расскажу вам одну историю о том, как смог прокачаться в качестве C++-программиста. Мне в этом помогло не чтение стандарта. Я тогда ещё не понимал до конца метапрограммирование с использованием шаблонов (честно говоря, прямо сейчас эту тему изучаю). Нет, просветление наступило, когда я всмотрелся в целую простыню кода на ассемблере x86–64, но не запаниковал, а подумал: “O, нет, нет. ЧТО ТАМ сделал компилятор?”

Читать вывод компилятора — это не какое-то мистическое тёмное искусство, которое практикуют только подстриженные в барбершопах разработчики компиляторов, с закрытыми глазами разбирающиеся в выделении регистров. Это навык. Его можно усвоить, затем в нём напрактиковаться, и результат вас очень удовлетворит. Овладев этим умением, вы больше никогда не будете писать «умные» абстракции, как раньше. 

Читая ассемблер, мы обращаем внимание на 4 вещи:

  • Векторизован ли код?

  • Есть ли в нём ветвления?

  • Вызывает ли он другой код?

  • Перезагружает ли он память?

Давайте разбираться.

Исходное положение: инструментарий для чтения ассемблера

Прямо сейчас вам понадобятся две вещи:

1. Compiler Explorer (godbolt.org)

Compiler Explorer — это бесплатный сайт, блестяще решающий ровно одну задачу: он берёт ваш код на C++, компилирует его в режиме реального времени с использованием любого компилятора и флагов на ваш выбор — и показывает вам ассемблер. Бок о бок с соответствующим C++.

Без преувеличения, это лучший инструмент в экосистеме C++, формально не входящий в экосистему C++.

2. Действительно важные флаги компилятора

Когда вы проверяете работу оживлённых участков кода, особенно важны следующие флаги:

# Рабочие лошадки
-O2          # "Ускорь это, но без безумных импровизаций"
-O3          # "Ускорь это, безумные импровизации в небольшом количестве допустимы"

# Векторизация
-march=native          # Используй все инструкции, поддерживаемые данным ЦП
-march=x86-64-v3       # Нацеливаемся конкретно на AVX2 (отлично помогает портировать код)

# Отладка оптимизатора
-fopt-info-vec         # Когда что-нибудь векторизуешь – сообщи мне об этом
-fopt-info-missed      # Сообщи мне, когда НЕ ОПТИМИЗИРОВАЛ, причём громко

# На локальной машине, для дальнейшей проверки:
objdump -d -M intel your_binary | less

При помощи флага -march=native вы, в принципе, сообщаете: «Компилятор, можешь пользоваться всеми фокусами из твоего арсенала, обещаю, что этот бинарник с моей машины никуда не денется». Для продакшена выбирайте что-то конкретное, а если хотите проверить, на что способен ваш ЦП — пользуйтесь  -march=native.

Читаем ассемблер без слёз: азбука

Примерно здесь в типичном туториале начинается отток читателей. Вам выдают триста строк на ассемблере AT&T, с синтаксисом, где повсюду рассыпаны знаки % — и вы закрываете вкладку. Мы же пойдём другим путём.

Две необходимые вещи, которые нужно напомнить об ассемблере x86–64 — и продолжим:

Синтаксис Intel лучше. Добавляйте к вашим флагам -masm=intel в GCC/Clang, либо просто воспользуйтесь выпадающим меню «Intel» в  Compiler Explorer. Оно читается слева направо.

Вот какие инструкции вы должны узнавать:

; Основы
mov  rax, rbx        ; копируем rbx в rax
add  rax, 8          ; rax += 8
cmp  rax, rbx        ; сравниваем, устанавливаем флаги
jl   .loop           ; если меньше - переходим (цикл!)


; Память
mov  rax, [rbx]      ; загружаем с адреса rbx
mov  [rbx], rax      ; сохраняем по адресу rbx


; Интересности
imul rax, rbx        ; Перемножение целых чисел
vaddps ymm0, ymm1    ; Складываем одновременно 8 чисел с плавающей точкой (SIMD!)
vmovups ymm0, [rsi]  ; Загружаем одновременно 8 чисел с плавающей точкой

Обратите внимание на последнюю группу — инструкции, начинающиеся с v и использующие такие регистры как ymm0 — в соответствии с тем, как у вас работает векторизация. Если вы их видите, это значит, что ваш компилятор одновременно оперирует 8 числами с плавающей точкой  (или 4 числами двойной точности или 16 короткими числами). А когда вы их не видите, тогда как ожидаете увидеть… тут и начинается самое интересное.

Пример 1: цикл, который не поддаётся векторизации (и почему)

Начнём с классического примера. Вы пишете совершенно нормальную функцию:

// Версия 1: безобидный цикл
void scale(float* data, float factor, int n) {
    for (int i = 0; i < n; i++) {
        data[i] *= factor;
    }
}

Просто! Умножаем каждый элемент на коэффициент. Конечно же, компилятор справляется с этим красиво, правда?

Компилируем с -O2 -march=x86-64-v3. Вот что получается (в сокращении):

; фактический вывод - O2 без ограничения
.loop:
    vmovss  xmm1, dword ptr [rdi + rax*4]   ; загружаем ОДНО float
    vmulss  xmm1, xmm1, xmm0                ; умножаем ОДНО float
    vmovss  dword ptr [rdi + rax*4], xmm1   ; сохраняем ОДНО float
    inc     rax
    cmp     rax, rdx
    jl      .loop

По одному. Числу с плавающей точкой. За операцию. Работаем с регистрами xmm, а не ymm. Этот код скалярный, а не векторизованный. Компилятор обрабатывает за итерацию одно число с плавающей точкой, а не восемь.

Почему? Поскольку data — это float*. Компилятор обеспокоен совмещением имён (aliasing). Что будет, если factor каким-то образом окажется в той же самой области памяти, что и data? Что, если, изменив data[0], мы тем самым изменим и значение factor? Это маловерятно, но, чисто технически, в стандарте это разрешено, поэтому компилятору приходится действовать осторожно.

«Но это же безумие, — скажете вы, — я бы так никогда не сделал». Но компилятор об этом не знает, так как вы ему не сообщили.

Правильно:

// Версия 2: с __restrict__ (или restrict в C99)
void scale(float* __restrict__ data, float factor, int n) {
    for (int i = 0; i < n; i++) {
        data[i] *= factor;
    }
}

Вы сообщаете: «Обещаю, что эти указатели совмещаться не будут. Слово чести».

Теперь посмотрим ассемблер:

; С __restrict__ - мы теперь cooking
.loop:
    vmovups ymm1, ymmword ptr [rdi + rax]   ; Загружаем ВОСЕМЬ чисел с плавающей точкой
    vmulps  ymm1, ymm1, ymm0                ; Перемножаем ВОСЕМЬ чисел с плавающей точкой
    vmovups ymmword ptr [rdi + rax], ymm1   ; Сохраняем ВОСЕМЬ чисел с плавающей точкой
    add     rax, 32                         ; Продвигаемся на 32 байта (8 чисел с плавающей точкой)
    cmp     rax, rcx
    jl      .loop

ymm регистры. vmulps (умножаем упакованные значения с плавающей точкой). Теперь компилятор обрабатывает 8 чисел с плавающей точкой за каждую итерацию. Всего один символ (__restrict__) даёт восьмикратное улучшение производительности данного цикла.

Это одна из тех вещей, благодаря которым я чувствую, чем занимаюсь.

Пример 2. Распознаём встраивание функций (или его подозрительное отсутствие)

Вот функция:

struct Vec3 {
    float x, y, z;
    float dot(const Vec3& other) const {
        return x * other.x + y * other.y + z * other.z;
    }
};

float compute(const Vec3& a, const Vec3& b) {
    return a.dot(b);
}

Как вы думаете, что произойдёт при -O2?

Хочется надеяться, что компилятор встроит dot в compute, и у вас получится плотная компактная последовательность операций сложения и умножения. Давайте проверим:

compute(Vec3 const&, Vec3 const&):
    vmovss  xmm0, dword ptr [rdi]        ; a.x
    vmulss  xmm0, xmm0, dword ptr [rsi] ; * b.x
    vmovss  xmm1, dword ptr [rdi + 4]   ; a.y
    vfmadd231ss xmm0, xmm1, dword ptr [rsi + 4]  ; += a.y * b.y
    vmovss  xmm1, dword ptr [rdi + 8]   ; a.z
    vfmadd231ss xmm0, xmm1, dword ptr [rsi + 8]  ; += a.z * b.z
    ret

Никакой инструкции call. Никакого перехода к Vec3::dot. Функция была полностью встроена — она просто загружает, умножает и умножает-складывает с однократным округлением (vfmadd231ss). Компилятор как насквозь увидел всю вашу абстракцию и выдал именно то, что автор в данном случае написал бы на ассемблере вручную.

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

А теперь рассмотрим такой пример встраивания. Добавим attribute((noinline)) к dot (или просто сделаем её virtual):

compute(Vec3 const&, Vec3 const&):
    ; ... сохраняем регистры, устанавливаем кадр вызова ...
    call    Vec3::dot(Vec3 const&) const
    ; ... восстанавливаем регистры ...
    ret

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

Урок: virtual даром не даётся. Равно как нельзя просто так помещать тела функций в файлы  .cpp, когда компилятор не может увидеть их на месте вызова. Компилятор может встраивать только то, что может видеть.

Пример 3. Неправильно скомпонованная структура, погубившая производительность

Эта проблема гораздо тоньше, и это мой любимый класс багов, поскольку в исходном коде они не видны.

// Казалось бы, безобидная структура
struct Particle {
    bool  active;    // 1 байт
    float x, y, z;  // 12 байт
    float vx, vy, vz; // 12 байт
    int   id;        // 4 байта
};

void update(Particle* particles, int n, float dt) {
    for (int i = 0; i < n; i++) {
        if (!particles[i].active) continue;
        particles[i].x += particles[i].vx * dt;
        particles[i].y += particles[i].vy * dt;
        particles[i].z += particles[i].vz * dt;
    }
}

Выглядит нормально. Скомпилируем это. Рассмотрим ассемблер.

Вы уже видите: никакой векторизации. Компилятор не может векторизовать сразу множество структур Particle, так как active — это булево значение bool внутри структуры. В цикле есть ветвление, причём данные xyz от разных частиц не лежат непрерывно — частично они дозаполнены нулями, а также между ними есть другие поля. Авто-векторизатор на этом сдаётся.

Вероятно, sizeof(Particle) имеет размер 32 байта, поскольку здесь применяется выравнивание. Итак, x  частицы 0 находится со сдвигом  4, x частицы — со сдвигом 36, x частицы 2 — со сдвигом 68... эти участки не образуют в памяти непрерывную область. А для SIMD нужны непрерывные данные.

Правильно: используем структуру массивов  (SoA), а не массив структур (AoS)

// Реорганизовано в духе дата-ориентированного проектирования
struct ParticleSystem {
    bool*  active;
    float* x;
    float* y;
    float* z;
    float* vx;
    float* vy;
    float* vz;
    int*   id;
    int    count;
};

void update(ParticleSystem& ps, float dt) {
    for (int i = 0; i < ps.count; i++) {
        if (!ps.active[i]) continue;
        ps.x[i] += ps.vx[i] * dt;
        ps.y[i] += ps.vy[i] * dt;
        ps.z[i] += ps.vz[i] * dt;
    }
}

Теперь все значения x расположены непрерывно. Все значения vx расположены непрерывно. Векторизатор может одновременно загрузить 8 значений x, одновременно 8 значений vx, перемножить их и добавить обратно. Ассемблер преображается из грустных скалярных загрузок в славные упакованные SIMD.

А если полностью отделить массив active и обрабатывать его в рамках предварительного прохода, потенциально можно вообще удалить ветку из внутреннего цикла.

На взгляд человека исходный код C++ выглядит хуже. Ассемблер кажется радикально лучше. Именно к такому компромиссу подталкивает нас дата-ориентированное проектирование, а чтобы проверить, насколько нам это преобразование подходит, нужно прочитать ассемблер.  

Пример 4. Небольшое изменение, а вывод совершенно другой

Покажу вам ещё одну вещь, которая по-прежнему вызывает у меня улыбку.

// Версия A
int sum_array(const int* arr, int n) {
    int total = 0;
    for (int i = 0; i < n; i++) {
        total += arr[i];
    }
    return total;
}

Скомпилировано с опцией -O3 -march=x86-64-v3:

; Векторизованный цикл редукции
.loop:
    vpaddd  ymm0, ymm0, ymmword ptr [rdi + rax]  ; складываем 8 целых чисел одновременно 
    add     rax, 32
    cmp     rax, rdx
    jl      .loop
; Сумма ymm0 по горизонтали...
vextracti128 xmm1, ymm0, 1
vpaddd       xmm0, xmm0, xmm1
vphaddd      xmm0, xmm0, xmm0
vphaddd      xmm0, xmm0, xmm0
vmovd        eax, xmm0
ret

Красиво. За одну итерацию складывается 8 целых чисел, а в конце суммируем по горизонтали.

А теперь — то самое «небольшое изменение»:

// Версия B – переполнение знаковых целых приводит к неопределённому поведению! 
// Компилятор ЗНАЕТ, что сумма не может приводить к критичным вариантам переполнения, 
// но такое впечатление складывается у него именно из-за неопределённого поведения 
int sum_array(const int* arr, int n) {
    int total = 0;
    for (int i = 0; i < n; i++) {
        total += arr[i];
        if (total < 0) break;  // При переполнении экстренно выходим
    }
    return total;
}

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

Нет, мораль здесь не в том, чтобы «не писать преждевременного выхода». Просто важно знать, что он имеет свою цену. Если профилировать этот цикл как горячий, то окажется, что его можно перестроить: сначала пустить векторизованный ход, а затем отдельно обработать условие переполнения. Ассемблер просигнализирует вам о проблеме, а вы уже сами думайте, как её исправить.

Как именно пользоваться этим на практике

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

1. Сначала профилируем. Находим, где у нас горячие точки. Под Mac для этого применяются  perf, VTune, Instruments — у вас на платформе могут быть другие инструменты. На данном этапе ищем тот 1% кода, на выполнение которого уходит 50% времени.

2. Изучаем эти горячие точки. Вставляем их в Compiler Explorer. Далее проверяем:

  • Используются ли во внутренних циклах xmm (скаляр) в случаях, когда вы ожидаете ymm (вектор)?

  • Видите ли вы инструкции call в большем количестве, чем ожидали? (может быть, встраивание не сработало)

  • Есть ли внутри цикла ветвление, которого там быть не должно?

  • Происходят ли операции загрузки и сохранения многократно с одними и теми же данными? (Может быть, подвёл анализ совмещения имён)

3. Измените что-то одно. Перекомпилируйте. Сравните. Это самое важное умение. Ровно одно изменение. Потом смотрим, пошло ли это на пользу ассемблеру. Не гадаем, проверяем.

4. Измеряйте. Если ассемблер выглядит лучше, это ещё не гарантирует, что он работает быстрее. Эффекты кэширования, неупорядоченное выполнение, предсказание ветвлений — всё это может смазать показатели, которые интуитивно выводятся по внешнему виду ассемблера. Всегда расставляйте бенчмарки, пользуйтесь при этом реалистичными объёмами данных.

Редфлаги, на которые следует обращать внимание при работе с ассемблером

Вот краткая сводка тех вещей, которые должны вас насторожить:

Вы видите

Это может означать

xmm в цикле, обрабатывающем значения с плавающей точкой

Нет векторизации. Проверьте, как дела с совмещением имён, ветвлением, есть ли разрывы при размещении данных в памяти

call внутри активного цикла

Встраивание не сработало. Проверьте, видит ли компилятор тело функции

[rsp + offset] внутри цикла

Сбросить данные в стек. Слишком много динамически изменяющихся переменных или таких значений, которые компилятору не удаётся держать в регистрах

Множество cmp + условных переходов

Код перегружен ветвлениями. Попробуйте альтернативный вариант без ветвлений

idiv или div

Целочисленное деление. Это медленные операции. Проверьте, можно ли заменить их перемещениями или умножением на обратное

lock xadd, lock cmpxcg

Атомарные операции. Нормально, если так и задумано, но страшно, если это вышло случайно

Вместо заключения

Компилятор C++ вам не враг. Его правильнее сравнить с чрезвычайно талантливым коллегой, который слегка перестраховывается. Он невероятно помогает вам — объединяет операции умножения, разматывает циклы, выполняет по восемь операций за раз — но лишь при условии, что всё это определённо безопасно. Чем дальше, тем сильнее ваша задача сводится к тому, чтобы уверить в этом компилятор.

restrict. Единообразно скомпонованные структуры. Тела функций хорошо видны. Расточительные ветвления в горячих циклах по возможности не допускаются. Ничто из этого не является экзотикой. Но вам ни за что не узнать, когда именно вам всё это понадобится, пока вы не посмотрите, что именно выдаёт вам компилятор.

Ассемблер не врёт. Врут ваши абстракции. Если вы стремитесь к высокой производительности, но не заглядываете в ассемблер, то вы оптимизируете вслепую.

Начните с одной функции. Откройте Compiler Explorer. Нажмите -O2. Посмотрите, что там. Возможно, вы ужаснётесь. Или восхититесь. Как бы то ни было, вы станете понимать ваш код так полно как никогда ранее.

И знаете, что ещё? Есть что-то глубоко приятное в том, когда смотришь на vmulps ymm0, ymm0, ymm1 — восемь операций умножения одновременно, полученных из простого *=в вашем C++ — и думаете: ого. А ведь получилось.

Приложение: полезные ссылки