Когда сборка происходит быстрее, чем C?


476

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

Этот вопрос даже не касается того факта, что инструкции на ассемблере будут специфичными для машины и непереносимыми, или каких-либо других аспектов ассемблера. Конечно, есть много веских причин для знания ассемблера, но это должен быть конкретный вопрос, требующий примеров и данных, а не расширенный дискурс по ассемблеру и языкам более высокого уровня.

Может ли кто-нибудь привести конкретные примеры случаев, когда сборка будет быстрее, чем хорошо написанный код C с использованием современного компилятора, и можете ли вы подтвердить это утверждение профилирующим доказательством? Я вполне уверен, что эти случаи существуют, но я действительно хочу точно знать, насколько эзотеричны эти случаи, так как это, кажется, является предметом некоторого спора.


17
на самом деле, улучшение скомпилированного кода довольно тривиально. Любой, кто хорошо знает язык ассемблера и Си, может убедиться в этом, изучив сгенерированный код. Любой легкий - это первый обрыв производительности, с которого вы падаете, когда у вас заканчиваются одноразовые регистры в скомпилированной версии. В среднем компилятор будет работать намного лучше, чем человек, для большого проекта, но в приличном размере не сложно найти проблемы с производительностью в скомпилированном коде.
old_timer

14
На самом деле, короткий ответ таков: Ассемблер всегда быстрее или равен скорости C. Причина в том, что у вас может быть сборка без C, но у вас не может быть C без сборки (в двоичной форме, которую мы в старой дни называются "машинный код"). Тем не менее, длинный ответ таков: компиляторы C довольно хорошо умеют оптимизировать и «думать» о вещах, о которых вы обычно не думаете, поэтому это действительно зависит от ваших навыков, но обычно вы всегда можете победить компилятор C; это все еще только программное обеспечение, которое не может думать и получать идеи. Вы также можете написать переносной ассемблер, если вы используете макросы и вы терпеливы.

11
Я категорически не согласен с тем, что ответы на этот вопрос должны быть «основаны на мнении» - они могут быть вполне объективными - это не то же самое, что пытаться сравнить эффективность любимых языков домашних животных, для которых у каждого будут свои сильные стороны и недостатки. Это вопрос понимания того, как далеко нас могут завести компиляторы, и с какой точки лучше их взять на себя.
jsbueno

21
В начале своей карьеры я много писал на Си и ассемблере для мэйнфреймов в софтверной компании. Одним из моих пэров был то, что я бы назвал «пуристом ассемблера» (все должно было быть ассемблером), поэтому я готов поспорить, что мог бы написать заданную процедуру, которая работала бы быстрее на C, чем та, которую он мог написать на ассемблере. Я выиграл. Но в довершение всего, после того, как я выиграл, я сказал ему, что хочу сделать вторую ставку - что я могу написать что-то более быстрое на ассемблере, чем программа C, которая победила его в предыдущей ставке. Я тоже выиграл, доказав, что большая часть этого сводится к навыкам и способностям программиста больше, чем что-либо еще.
Валери Р

3
Если у вашего мозга нет -O3флага, вам, вероятно, лучше оставить оптимизацию для компилятора C :-)
paxdiablo

Ответы:


273

Вот реальный пример: умножение с фиксированной запятой на старых компиляторах.

Они не только пригодятся на устройствах без плавающей запятой, они сияют, когда дело доходит до точности, поскольку они дают 32 бита точности с предсказуемой ошибкой (у плавающего есть только 23 бита, и труднее предсказать потерю точности). т. е. равномерная абсолютная точность по всему диапазону вместо почти одинаковой относительной точности ( float).


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

  • Получение большей части 64-битного целочисленного умножения : портативная версия, использующая uint64_t32x32 => 64-битное умножение, не может быть оптимизирована на 64-битном процессоре, поэтому вам нужны встроенные или __int128эффективный код на 64-битных системах.
  • _umul128 в Windows 32 бит : MSVC не всегда хорошо справляется с умножением 32-битных целых чисел, приведенных к 64, поэтому встроенные функции очень помогли.

C не имеет оператора полного умножения (2N-битный результат из N-битных входов). Обычный способ выразить это в C - привести входные данные к более широкому типу и надеяться, что компилятор распознает, что старшие биты входных данных не интересны:

// on a 32-bit machine, int can hold 32-bit fixed-point integers.
int inline FixedPointMul (int a, int b)
{
  long long a_long = a; // cast to 64 bit.

  long long product = a_long * b; // perform multiplication

  return (int) (product >> 16);  // shift by the fixed point bias
}

Проблема с этим кодом заключается в том, что мы делаем то, что не может быть прямо выражено на языке Си. Мы хотим умножить два 32-битных числа и получить 64-битный результат, из которого мы возвращаем средний 32-битный. Однако в C это умножение не существует. Все, что вы можете сделать, это повысить целые числа до 64 бит и сделать умножение 64 * 64 = 64.

Однако x86 (и ARM, MIPS и другие) могут выполнять умножение в одной инструкции. Некоторые компиляторы игнорировали этот факт и генерировали код, который вызывает функцию библиотеки времени выполнения для выполнения умножения. Сдвиг на 16 также часто выполняется библиотечной подпрограммой (такой же сдвиг может выполнять и x86).

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

Если вы переписываете тот же код на (встроенном) ассемблере, вы можете значительно увеличить скорость.

В дополнение к этому: использование ASM - не лучший способ решения проблемы. Большинство компиляторов позволяют вам использовать некоторые инструкции на ассемблере во внутренней форме, если вы не можете выразить их в C. Например, компилятор VS.NET2008 выставляет 32 * 32 = 64-битное значение mul как __emul, а 64-битное смещение как __ll_rshift.

Используя встроенные функции, вы можете переписать функцию таким образом, чтобы у C-компилятора была возможность понять, что происходит. Это позволяет встроить код, распределить регистр, исключить общее подвыражение и постоянное распространение. Вы получите огромныйТаким образом, улучшение производительности по сравнению с рукописным ассемблерным кодом.

Для справки: конечный результат для mul с фиксированной точкой для компилятора VS.NET:

int inline FixedPointMul (int a, int b)
{
    return (int) __ll_rshift(__emul(a,b),16);
}

Разница в производительности делителей с фиксированной точкой еще больше. У меня были улучшения до 10 раз для тяжелого кода с фиксированной запятой, написав пару asm-строк.


Использование Visual C ++ 2013 дает одинаковый код сборки для обоих способов.

gcc4.1 из 2007 также хорошо оптимизирует версию на чистом C. (В проводнике компилятора Godbolt не было установлено более ранних версий gcc, но, вероятно, даже более старые версии GCC могли бы делать это без встроенных функций.)

См. Source + asm для x86 (32-бит) и ARM в проводнике компилятора Godbolt . (К сожалению, у него нет достаточно старых компиляторов для создания плохого кода из простой версии на чистом C).


Современные процессоры могут делать вещи C не имеют операторов для вообще , как popcntи битые-сканирование , чтобы найти первый или последний набор бит . (POSIX имеет ffs()функцию, но ее семантика не соответствует x86 bsf/ bsr. См. Https://en.wikipedia.org/wiki/Find_first_set ).

Некоторые компиляторы могут иногда распознавать цикл, который подсчитывает количество установленных битов в целом числе и компилировать его в popcntинструкцию (если она включена во время компиляции), но гораздо надежнее использовать __builtin_popcntв GNU C или в x86, если вы только нацеливание оборудования с SSE4.2: _mm_popcnt_u32от<immintrin.h> .

Или в C ++, присвойте std::bitset<32>и используйте .count(). (Это тот случай, когда язык нашел способ портативного представления оптимизированной реализации popcount через стандартную библиотеку, таким образом, который всегда будет компилироваться во что-то правильное и может использовать все, что поддерживает цель.) См. Также https : //en.wikipedia.org/wiki/Hamming_weight#Language_support .

Аналогично, ntohlможет компилироваться в bswap(32-битный байт подкачки x86 для преобразования в порядковый номер) в некоторых реализациях C, которые его имеют.


Другая важная область для встроенных функций или рукописного асма - это ручная векторизация с инструкциями SIMD. Компиляторы не плохи с простыми циклами, как dst[i] += src[i] * 10.0;, но часто плохо или вообще не векторизации, когда все становится сложнее. Например, вы вряд ли получите что-то вроде Как реализовать Atoi с помощью SIMD? автоматически генерируется компилятором из скалярного кода.


6
Как насчет таких вещей, как {x = c% d; y = c / d;}, достаточно ли умны компиляторы, чтобы сделать это одним div или idiv?
Йенс Бьорнхагер

4
На самом деле, хороший компилятор выдает оптимальный код из первой функции. Затухание исходного кода с помощью встроенных или встроенных сборок без какой-либо выгоды - не лучшая вещь.
бездельник

65
Привет, бездельник, я думаю, тебе никогда не приходилось работать над критическим по времени кодом ... встроенная сборка может иметь * огромное значение. Кроме того, для компилятора встроенная функция аналогична обычной арифметике в Си. В этом суть встроенной функции. Они позволяют вам использовать архитектурную функцию, не имея дело с недостатками.
Нильс Пипенбринк

6
@slacker На самом деле код здесь вполне читабелен: встроенный код выполняет одну уникальную операцию, которая сразу же становится нестабильной при чтении сигнатуры метода. Код только медленно теряется в удобочитаемости, когда используется неясная инструкция. Здесь важно то, что у нас есть метод, который выполняет только одну четко идентифицируемую операцию, и это действительно лучший способ создать читаемый код этих атомарных функций. Кстати, такой небольшой комментарий, как / * (a * b) >> 16 * /, не так неясен.
Дерексон

5
Чтобы быть справедливым, это пример плохой, по крайней мере сегодня. Компиляторы C уже давно могут умножать 32x32 -> 64, даже если язык не предлагает этого напрямую: они признают, что когда вы приводите 32-битные аргументы к 64-битным, а затем умножаете их, в этом нет необходимости сделать полное 64-битное умножение, но 32x32 -> 64 подойдет. Я проверил, и все clang, gcc и MSVC в их текущей версии понимают это правильно . Это не ново - я помню, как смотрел на результаты компиляции и заметил это десять лет назад.
BeeOnRope

143

Много лет назад я учил кого-то программировать на C. Упражнение состояло в том, чтобы повернуть изображение на 90 градусов. Он вернулся с решением, которое заняло несколько минут, в основном потому, что он использовал умножения и деления и т. Д.

Я показал ему, как исправить проблему, используя сдвиги битов, и время для обработки на неоптимизирующем компиляторе, которое он имел, сократилось примерно до 30 секунд.

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


3
Да, это была одноразрядная монохромная система, в частности это были монохромные блоки изображений на Atari ST.
Лилберн

16
Оптимизирующий компилятор компилировал оригинальную программу или вашу версию?
Турбьёрн Равн Андерсен

На каком процессоре? На 8086 я ожидал, что оптимальный код для поворота 8x8 будет загружать DI с 16 битами данных, используя SI, повторять add di,di / adc al,al / add di,di / adc ah,ahи т. Д. Для всех восьми 8-битных регистров, затем снова делать все 8 регистров и затем повторять всю процедуру три еще раз, и, наконец, сохранить четыре слова в Ax / BX / CX / DX. Никоим образом ассемблер не приблизится к этому.
Суперкат

1
Я действительно не могу представить себе какую-либо платформу, в которой компилятор мог бы получить коэффициент или два от оптимального кода для поворота 8x8.
Суперкат

65

Практически всегда, когда компилятор видит код с плавающей запятой, рукописная версия будет быстрее, если вы используете старый плохой компилятор. ( Обновление 2019 года: в целом это не так для современных компиляторов. Особенно при компиляции для чего-либо, кроме x87; компиляторам проще работать с SSE2 или AVX для скалярной математики или с любыми не x86 с плоским регистром FP, в отличие от х87 стек регистров.)

Основная причина заключается в том, что компилятор не может выполнять какие-либо робастные оптимизации. Смотрите эту статью из MSDN для обсуждения на эту тему. Вот пример, где версия сборки в два раза быстрее, чем версия C (скомпилирована с VS2K5):

#include "stdafx.h"
#include <windows.h>

float KahanSum(const float *data, int n)
{
   float sum = 0.0f, C = 0.0f, Y, T;

   for (int i = 0 ; i < n ; ++i) {
      Y = *data++ - C;
      T = sum + Y;
      C = T - sum - Y;
      sum = T;
   }

   return sum;
}

float AsmSum(const float *data, int n)
{
  float result = 0.0f;

  _asm
  {
    mov esi,data
    mov ecx,n
    fldz
    fldz
l1:
    fsubr [esi]
    add esi,4
    fld st(0)
    fadd st(0),st(2)
    fld st(0)
    fsub st(0),st(3)
    fsub st(0),st(2)
    fstp st(2)
    fstp st(2)
    loop l1
    fstp result
    fstp result
  }

  return result;
}

int main (int, char **)
{
  int count = 1000000;

  float *source = new float [count];

  for (int i = 0 ; i < count ; ++i) {
    source [i] = static_cast <float> (rand ()) / static_cast <float> (RAND_MAX);
  }

  LARGE_INTEGER start, mid, end;

  float sum1 = 0.0f, sum2 = 0.0f;

  QueryPerformanceCounter (&start);

  sum1 = KahanSum (source, count);

  QueryPerformanceCounter (&mid);

  sum2 = AsmSum (source, count);

  QueryPerformanceCounter (&end);

  cout << "  C code: " << sum1 << " in " << (mid.QuadPart - start.QuadPart) << endl;
  cout << "asm code: " << sum2 << " in " << (end.QuadPart - mid.QuadPart) << endl;

  return 0;
}

И некоторые цифры с моего ПК, на котором запущена версия по умолчанию * :

  C code: 500137 in 103884668
asm code: 500137 in 52129147

Из интереса я поменял цикл с помощью dec / jnz, и это не имело никакого значения для времени - иногда быстрее, иногда медленнее. Я предполагаю, что ограниченный объем памяти затмевает другие оптимизации. (Примечание редактора: более вероятно, что узкое место задержки FP достаточно, чтобы скрыть дополнительные затраты loop. Выполнение двух сумм Кахана параллельно для нечетных / четных элементов и добавление их в конце может ускорить это в 2 раза. )

Ой, я запустил немного другую версию кода, и он вывел числа неправильно (т.е. C был быстрее!). Исправлены и обновлены результаты.


20
Или в GCC, вы можете развязать руки компилятора по оптимизации с плавающей запятой (если вы пообещаете ничего не делать с бесконечностями или NaN), используя флаг -ffast-math. У них есть уровень оптимизации, -Ofastкоторый в настоящее время эквивалентен -O3 -ffast-math, но в будущем может включать в себя больше оптимизаций, которые могут привести к неправильной генерации кода в угловых случаях (таких как код, основанный на NaN IEEE).
Дэвид Стоун

2
Да, поплавки не коммутативны, компилятор должен делать именно то, что вы написали, в основном то, что сказал @DavidStone.
Алек Тил

2
Вы пробовали математику SSE? Производительность была одной из причин, по которой MS полностью отказалась от x87 в x86_64 и на 80-битной двойной в x86
phuclv

4
@Praxeolitic: FP add является коммутативным ( a+b == b+a), но не ассоциативным (переупорядочение операций, поэтому округление промежуточных соединений отличается). Re: этот код: я не думаю, что без комментариев x87 и loopинструкция очень удивительная демонстрация fast asm. loopочевидно, на самом деле не является узким местом из-за задержки FP. Я не уверен, конвейеризует ли он операции FP или нет; x87 трудно читать людям. Два fstp resultsinsns в конце явно не оптимальны. Вытащить дополнительный результат из стека было бы лучше сделать с не магазином. Как и fstp st(0)IIRC.
Питер Кордес

2
@PeterCordes: Интересное следствие создания коммутативного сложения состоит в том, что хотя 0 + x и x + 0 эквивалентны друг другу, ни один из них не всегда эквивалентен x.
суперкат

58

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

В общем случае современный компилятор C знает гораздо больше о том, как оптимизировать рассматриваемый код: он знает, как работает конвейер процессора, он может попытаться переупорядочить инструкции быстрее, чем это может сделать человек, и так далее - это в основном так же, как компьютер так же хорош или лучше, чем лучший игрок в настольные игры и т. д. просто потому, что он может выполнять поиск в проблемном пространстве быстрее, чем большинство людей. Хотя вы теоретически можете работать так же хорошо, как и компьютер в конкретном случае, вы, конечно, не можете делать это с той же скоростью, что делает его невозможным для более чем нескольких случаев (т. Е. Компилятор наверняка превзойдет вас, если вы попытаетесь написать несколько процедур в ассемблере).

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

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


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

1
@Liedman: «он может попытаться изменить порядок команд быстрее, чем человек». OCaml известен своей быстротой, и, что удивительно, его компилятор с собственным кодом ocamloptпропускает планирование команд на x86 и вместо этого оставляет его на ЦП, потому что он может более эффективно переупорядочивать во время выполнения.
Джон Харроп

1
Современные компиляторы делают много, и это заняло бы слишком много времени, но они далеко не идеальны. Ищите в gcc или в трекерах llvm ошибки "missed-оптимизация". Есть много. Кроме того, при написании в asm вы можете легче использовать предварительные условия, такие как «этот ввод не может быть отрицательным», которые компилятору будет сложно доказать.
Питер Кордес

48

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

  1. Отладка - я часто получаю библиотечный код с ошибками или неполной документацией. Я выясняю, что он делает, вступая на уровне сборки. Я должен делать это примерно раз в неделю. Я также использую его как инструмент для отладки проблем, в которых мои глаза не замечают идиоматическую ошибку в C / C ++ / C #. Глядя на сборку, это проходит.

  2. Оптимизация - компилятор неплохо справляется с оптимизацией, но я играю не так, как большинство. Я пишу код обработки изображений, который обычно начинается с кода, который выглядит следующим образом:

    for (int y=0; y < imageHeight; y++) {
        for (int x=0; x < imageWidth; x++) {
           // do something
        }
    }

    «сделать что-то часть» обычно происходит порядка нескольких миллионов раз (т. е. от 3 до 30). Соскребая циклы в этой фазе «сделать что-то», выигрыш в производительности значительно увеличивается. Я обычно не начинаю там - я обычно начинаю с того, что сначала пишу код для работы, а затем делаю все возможное, чтобы реорганизовать C, чтобы он был естественно лучше (лучший алгоритм, меньшая нагрузка в цикле и т. Д.). Мне обычно нужно читать ассемблер, чтобы увидеть, что происходит, и редко нужно его писать. Я делаю это возможно каждые два или три месяца.

  3. делать то, что язык не позволит мне. К ним относятся - получение архитектуры процессора и конкретных функций процессора, доступ к флагам, не входящим в процессор (мужик, я действительно хотел бы, чтобы C предоставил вам доступ к флагу переноса) и т. Д. Я делаю это, возможно, раз в год или два года.


Вы не укладываете петли? :-)
Джон Харроп

1
@plinth: как вы понимаете "циклы соскабливания"?
lang2

@ lang2: это означает избавление от как можно большего количества лишнего времени, проведенного во внутреннем цикле - всего, что компилятору не удалось вытащить, что может включать использование алгебры для поднятия коэффициента умножения из одного цикла, чтобы сделать его сложным во внутреннем и т. д.
плинтус

1
Циклическое разбиение кажется ненужным, если вы делаете только один проход по данным.
Джеймс М. Лей

@ JamesM.Lay: Если вы касаетесь каждого элемента только один раз, лучший порядок обхода может дать вам пространственную локализацию. (например, используйте все байты строки кэша, к которой вы прикоснулись, вместо того, чтобы зацикливать столбцы матрицы, используя один элемент на строку кэша.)
Питер Кордес,

42

Только при использовании некоторых специальных наборов инструкций компилятор не поддерживает.

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

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


4
+1, хотя последнее предложение на самом деле не относится к этому обсуждению - можно предположить, что ассемблер вступает в игру только после того, как все возможные улучшения алгоритма и т. Д. Были реализованы.
Mghie

18
@Matt: рукописный ASM часто намного лучше на некоторых крошечных процессорах, с которыми EE работает, которые имеют дрянную поддержку компилятора вендора.
Zan Lynx

5
«Только при использовании некоторых наборов инструкций специального назначения» ?? Вы, вероятно, никогда ранее не писали кусочек оптимизированного вручную ассемблерного кода. Умеренное знание архитектуры, над которой вы работаете, дает вам хороший шанс сгенерировать лучший код (размер и скорость), чем ваш компилятор. Очевидно, что, как прокомментировал @mghie, вы всегда начинаете кодировать лучшие алгоритмы, с которыми вы можете столкнуться для решения вашей проблемы. Даже для очень хороших компиляторов вы действительно должны писать свой код на C так, чтобы компилятор получал лучший скомпилированный код. В противном случае сгенерированный код будет неоптимальным.
ysap

2
@ysap - на реальных компьютерах (а не на крошечных встроенных микросхемах с низким энергопотреблением) в реальном мире «оптимальный» код не будет быстрее, потому что для любого большого набора данных ваша производительность будет ограничена доступом к памяти и ошибками страниц ( и если у вас нет большого набора данных, это будет быстрым в любом случае, и нет смысла его оптимизировать) - в те дни я работаю в основном на C # (даже не на c), и выигрыш в производительности от сжатия менеджера памяти взвалить на себя затраты на сборку мусора, сжатие и JIT-компиляцию.
Нир

4
+1 за утверждение, что компиляторы (особенно JIT) могут работать лучше, чем люди, если они оптимизированы под оборудование, на котором они работают.
Себастьян

38

Хотя C "близок" к низкоуровневой обработке 8-битных, 16-битных, 32-битных, 64-битных данных, есть несколько математических операций, не поддерживаемых C, которые часто могут выполняться элегантно в определенных инструкциях сборки наборы:

  1. Умножение с фиксированной запятой: произведение двух 16-битных чисел представляет собой 32-битное число. Но правила в Си говорят, что произведение двух 16-битных чисел - это 16-битное число, а произведение двух 32-битных чисел - это 32-битное число - нижняя половина в обоих случаях. Если вы хотите, чтобы верхняя половина умножалась на 16х16 или умножалась на 32х32, вы должны играть в игры с компилятором. Общий метод заключается в приведении к битовой ширине, которая больше необходимой, умножении, сдвиге вниз и приведении назад:

    int16_t x, y;
    // int16_t is a typedef for "short"
    // set x and y to something
    int16_t prod = (int16_t)(((int32_t)x*y)>>16);`

    В этом случае компилятор может быть достаточно умен, чтобы знать, что вы на самом деле просто пытаетесь получить верхнюю половину умножения 16x16 и делать правильные вещи с собственным умножением 16x16m. Или это может быть глупо и требовать, чтобы библиотечный вызов умножил 32x32, что слишком много, потому что вам нужно всего лишь 16 бит продукта - но стандарт C не дает вам никакого способа выразить себя.

  2. Определенные операции сдвига битов (ротация / переносы):

    // 256-bit array shifted right in its entirety:
    uint8_t x[32];
    for (int i = 32; --i > 0; )
    {
       x[i] = (x[i] >> 1) | (x[i-1] << 7);
    }
    x[0] >>= 1;

    Это не слишком не элегантно в C, но, опять же, если компилятор не достаточно умен, чтобы понимать, что вы делаете, он будет выполнять много «ненужной» работы. Многие наборы инструкций по сборке позволяют вращать или сдвигать влево / вправо с результатом в регистре переноса, поэтому вы можете выполнить вышеизложенное в 34 инструкциях: загрузить указатель на начало массива, очистить перенос и выполнить 32. сдвиг вправо, используя автоинкремент по указателю.

    В другом примере есть регистры сдвига с линейной обратной связью (LFSR), которые элегантно выполняются в сборке: возьмите блок из N битов (8, 16, 32, 64, 128 и т. Д.), Сдвиньте все это на 1 (см. Выше). алгоритма), тогда, если результирующий перенос равен 1, тогда вы XOR в битовой комбинации, которая представляет полином.

Сказав это, я бы не прибегал к этим методам, если у меня не было серьезных ограничений производительности. Как уже говорили другие, сборка намного сложнее документировать / отлаживать / тестировать / поддерживать, чем код C: выигрыш в производительности сопряжен с серьезными затратами.

редактировать: 3. Обнаружение переполнения возможно в сборке (на самом деле не может сделать это в C), это делает некоторые алгоритмы намного проще.


23

Короткий ответ? Иногда.

Технически каждая абстракция имеет свою стоимость, а язык программирования - это абстракция работы процессора. С однако очень близко. Несколько лет назад я помню, как громко смеялся, когда я вошел в свою учетную запись UNIX и получил следующее сообщение об удаче (когда такие вещи были популярны):

Язык программирования C - язык, который сочетает в себе гибкость языка ассемблера и силу языка ассемблера.

Забавно, потому что это правда: C похож на портативный ассемблер.

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

Когда появился gcc, одна из вещей, которые сделали его настолько популярным, это то, что он часто был намного лучше, чем компиляторы C, которые поставлялись со многими коммерческими разновидностями UNIX. Мало того, что это был ANSI C (ни один из этих мусоров K & R C), он был более надежным и, как правило, создавал лучший (более быстрый) код. Не всегда, но часто.

Я говорю вам все это, потому что нет общего правила о скорости C и ассемблере, потому что нет никакого объективного стандарта для C.

Точно так же, ассемблер сильно различается в зависимости от того, какой процессор вы используете, какие у вас системные характеристики, какой набор инструкций вы используете и так далее. Исторически существовало два семейства процессорных архитектур: CISC и RISC. Самым крупным игроком в CISC была и остается архитектура Intel x86 (и набор инструкций). RISC доминировал в мире UNIX (MIPS6000, Alpha, Sparc и т. Д.). CISC выиграл битву за сердца и умы.

Во всяком случае, когда я был более молодым разработчиком, распространенной мудростью было то, что рукописный x86 часто мог быть намного быстрее, чем C, потому что, как работает архитектура, он имел сложность, которая приносила пользу человеку. RISC, с другой стороны, казался разработанным для компиляторов, поэтому никто (я знал) не писал, скажем, Sparc на ассемблере. Я уверен, что такие люди существовали, но, без сомнения, они оба сошли с ума и к настоящему моменту были институционализированы.

Наборы инструкций являются важным моментом даже в одном семействе процессоров. Некоторые процессоры Intel имеют такие расширения, как SSE - SSE4. У AMD были свои собственные инструкции SIMD. Преимущество такого языка программирования, как C, заключается в том, что кто-то может написать свою библиотеку, чтобы она была оптимизирована для любого процессора, на котором вы работали. Это была тяжелая работа на ассемблере.

В ассемблере по-прежнему есть оптимизации, которые не может сделать ни один компилятор, и хорошо написанный алгоритм ассемблера будет таким же быстрым или быстрым, как его эквивалент в Си. Большой вопрос: стоит ли это того?

В конечном счете, хотя ассемблер был продуктом своего времени и был более популярен в то время, когда циклы ЦП были дорогими. В настоящее время процессор, стоимость которого составляет 5-10 долларов (Intel Atom), может делать практически все, что угодно. Единственная реальная причина для написания ассемблера в наши дни - это низкоуровневые вещи, такие как некоторые части операционной системы (несмотря на то, что подавляющее большинство ядра Linux написано на C), драйверы устройств, возможно встроенные устройства (хотя C имеет тенденцию доминировать там). тоже) и тд. Или просто для ударов (что несколько мазохистски).


Было много людей, которые использовали ассемблер ARM в качестве языка выбора на машинах Acorn (начало 90-х). IIRC сказали, что небольшой набор инструкций по рискам делает его более легким и увлекательным. Но я подозреваю, что это потому, что компилятор C опоздал на Acorn, а компилятор C ++ так и не был завершен.
Андрей М

3
«... потому что нет субъективного стандарта для C.» Вы имеете в виду цель .
Томас

@AndrewM: Да, я писал смешанные приложения на ассемблере BASIC и ARM около 10 лет. В то время я выучил C, но это было не очень полезно, потому что он такой же громоздкий, как ассемблер, и медленнее. Norcroft сделал несколько потрясающих оптимизаций, но я думаю, что набор условных команд был проблемой для компиляторов того времени.
Джон Харроп

1
@AndrewM: ну, на самом деле ARM - это своего рода RISC, сделанный задом наперед. Другие RISC ISA были разработаны с учетом того, что будет использовать компилятор. ARM ISA, кажется, был разработан, исходя из того, что предоставляет процессор (смещение ствола, флаги условий → давайте выставим их в каждой инструкции).
ниндзя

16

Вариант использования, который может больше не применяться, но для вашего удовольствия: на Amiga ЦП и графические / аудио чипы будут бороться за доступ к определенной области ОЗУ (первые 2 МБ ОЗУ будут специфическими). Поэтому, когда у вас было только 2 МБ ОЗУ (или меньше), отображение сложной графики и воспроизведение звука снизили бы производительность процессора.

В ассемблере вы можете чередовать свой код таким умным способом, что ЦП будет пытаться получить доступ к ОЗУ только тогда, когда графические / аудиочипы заняты внутри (т.е. когда шина была свободна). Таким образом, переупорядочивая ваши инструкции, умело используя кэш ЦП, синхронизацию шины, вы могли достичь некоторых эффектов, которые были просто невозможны при использовании любого языка более высокого уровня, потому что вам приходилось синхронизировать каждую команду, даже вставлять NOP здесь и там, чтобы сохранить различные чипы друг от друга радар.

Это еще одна причина, по которой инструкция ЦПУ NOP (Без операции - ничего не делать) может на самом деле ускорить работу всего приложения.

[РЕДАКТИРОВАТЬ] Конечно, техника зависит от конкретной настройки оборудования. Это было основной причиной, по которой многие игры Amiga не могли справиться с более быстрыми процессорами: время выполнения инструкций было неверным.


В Amiga не было 16 МБ оперативной памяти, больше от 512 до 2 МБ в зависимости от чипсета. Кроме того, многие игры Amiga не работают с более быстрыми процессорами из-за описанных вами методов.
bk1e

1
@ bk1e - Amiga произвела большое количество различных моделей компьютеров, в моем случае Amiga 500 поставлялась с оперативной памятью 512K, увеличенной до 1Meg. amigahistory.co.uk/amiedevsys.html - это амига со 128Мег рамом
Дэвид Уотерс

@ bk1e: Я исправлен. Моя память может подвести меня, но не ограничена ли память ОЗУ первым 24-битным адресным пространством (т.е. 16 МБ)? И Фаст был нанесен на карту выше этого?
Аарон Дигулла

@ Аарон Дигулла: В Википедии есть больше информации о различиях между чипом / быстрой / медленной RAM: en.wikipedia.org/wiki/Amiga_Chip_RAM
bk1e

@ bk1e: Моя ошибка. Процессор 68k имел только 24 адресных линии, поэтому у меня в голове было 16 МБ.
Аарон Дигулла

15

Укажите один, который не является ответом.
Даже если вы никогда не программируете это, я считаю полезным знать хотя бы один набор инструкций на ассемблере. Это часть бесконечного стремления программистов знать больше и, следовательно, быть лучше. Также полезно, когда вы заходите в фреймворки, у вас нет исходного кода и, по крайней мере, неточно понимаете, что происходит. Это также поможет вам понять JavaByteCode и .Net IL, так как они похожи на ассемблер.

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

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

Мой друг работает над микроконтроллерами, в настоящее время чипами для управления маленькими электродвигателями. Он работает в комбинации низкого уровня c и Assembly. Однажды он рассказал мне о хорошем дне на работе, когда он сократил основной цикл с 48 инструкций до 43. Он также столкнулся с выбором, например, с расширением кода для заполнения чипа 256 Кбайт, и бизнес хочет новую функцию, не так ли?

  1. Удалить существующую функцию
  2. Уменьшите размер некоторых или всех существующих функций, возможно, за счет снижения производительности.
  3. Пропагандируйте переход на более крупную микросхему с более высокой стоимостью, большим энергопотреблением и большим форм-фактором.

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

Я знаю, что гораздо больше ответил на вопрос «почему я должен изучать ассемблер», но я чувствую, что это более важный вопрос, чем когда он быстрее.

так что давайте попробуем еще раз. Вы должны думать о сборке

  • работает на низком уровне функции операционной системы
  • Работаю над компилятором.
  • Работа на чрезвычайно ограниченном чипе, встроенной системе и т. Д.

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

Дэвид.


4
+1 за рассмотрение встроенных приложений на крошечных чипсах. Слишком много инженеров-программистов здесь либо не рассматривают встроенные, либо считают, что это означает смартфон (32-битный, МБ ОЗУ, МБ флэш-память).
Мартин

1
Временные приложения являются отличным примером! Часто встречаются странные инструкции (даже очень простые, такие как avr sbiи cbi), которые компиляторы привыкли (а иногда и делают) не в полной мере воспользоваться из-за ограниченного знания аппаратного обеспечения.
Феликсфью

15

Я удивлен, что никто не сказал это. strlen()Функция гораздо быстрее , если написано в сборе! В C лучшее, что вы можете сделать, это

int c;
for(c = 0; str[c] != '\0'; c++) {}

во время сборки вы можете значительно ускорить его:

mov esi, offset string
mov edi, esi
xor ecx, ecx

lp:
mov ax, byte ptr [esi]
cmp al, cl
je  end_1
cmp ah, cl
je end_2
mov bx, byte ptr [esi + 2]
cmp bl, cl
je end_3
cmp bh, cl
je end_4
add esi, 4
jmp lp

end_4:
inc esi

end_3:
inc esi

end_2:
inc esi

end_1:
inc esi

mov ecx, esi
sub ecx, edi

длина в ecx. Это сравнивает 4 символа за раз, так что это в 4 раза быстрее. И подумайте, используя старшее слово eax и ebx, оно станет в 8 раз быстрее , чем предыдущая процедура C!


3
Как это соотносится с теми, что указаны в strchr.nfshost.com/optimized_strlen_function ?
ниндзя

@ninjalj: это одно и то же :) я не думал, что это можно сделать таким образом в C. Это может быть немного улучшено, я думаю
BlackBear

Перед каждым сравнением в коде C по-прежнему есть побитовая операция AND. Вполне возможно, что компилятор будет достаточно умен, чтобы уменьшить его до сравнений старшего и младшего байтов, но я бы не стал ставить на это деньги. На самом деле существует более быстрый алгоритм цикла, основанный на (word & 0xFEFEFEFF) & (~word + 0x80808080)нулевом свойстве, если все байты в слове не равны нулю.
user2310967

@MichaWiedenmann true, я должен загрузить bx после сравнения двух символов в топоре. Спасибо
BlackBear

14

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


Некоторые компиляторы (VectorC, если я правильно помню) генерируют SIMD-код, так что даже это, вероятно, больше не является аргументом для использования ассемблерного кода.
OregonGhost

Компиляторы создают код с поддержкой SSE, так что аргумент
неверен

5
Для многих из этих ситуаций вы можете использовать SSE intrisics вместо сборки. Это сделает ваш код более переносимым (gcc visual c ++, 64-битный, 32-битный и т. Д.), И вам не нужно делать регистры.
Laserallan

1
Конечно, вы бы хотели, но вопрос не спрашивал, где я должен использовать ассемблер вместо C. В нем говорилось, что компилятор C не генерирует лучший код. Я предположил источник C, который не использует прямые вызовы SSE или встроенную сборку.
Мердад Афшари

9
Мердад прав, хотя. Правильно настроить SSE довольно сложно для компилятора, и даже в очевидных (для человека) ситуациях большинство компиляторов не используют его.
Конрад Рудольф

13

Я не могу привести конкретные примеры, потому что это было слишком много лет назад, но было много случаев, когда рукописный ассемблер мог превзойти любой компилятор. Причины, почему:

  • Вы можете отклониться от соглашений о вызовах, передавая аргументы в регистрах.

  • Вы можете тщательно продумать, как использовать регистры, и избежать хранения переменных в памяти.

  • Для таких вещей, как таблицы переходов, вы можете избежать проверки границ индекса.

В основном, компиляторы выполняют довольно хорошую работу по оптимизации, и это почти всегда «достаточно хорошо», но в некоторых ситуациях (например, рендеринг графики), где вы платите дорого за каждый отдельный цикл, вы можете использовать ярлыки, потому что вы знаете код где компилятор не мог, потому что он должен быть на безопасной стороне.

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

Тем не менее, я хочу, чтобы компилятор генерировал хороший ассемблерный код для меня, но не был слишком умным, и они в основном это делают. Фактически, одна из вещей, которые я ненавижу в Фортране, - это шифрование кода в попытке «оптимизировать» его, как правило, без существенной цели.

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

Добавлено: я видел множество приложений, написанных на ассемблере, и главное преимущество в скорости по сравнению с такими языками, как C, Pascal, Fortran и т. Д., Было в том, что программист был намного осторожнее при кодировании на ассемблере. Он или она собирается писать примерно 100 строк кода в день, независимо от языка, и на языке компилятора, который будет равен 3 или 400 инструкциям.


8
+1: «Вы можете отклониться от условностей вызова». Компиляторы C / C ++ имеют тенденцию отстой при возвращении нескольких значений. Они часто используют форму sret, где стек вызывающей стороны выделяет непрерывный блок для структуры и передает ссылку на нее вызываемому объекту, чтобы заполнить его. Возвращение нескольких значений в регистрах происходит в несколько раз быстрее.
Джон Харроп

1
@Jon: компиляторы C / C ++ прекрасно справляются с этой задачей, когда функция становится встроенной (не встроенные функции должны соответствовать ABI, это не ограничение C и C ++, а модель связывания)
Бен Фойгт,

@BenVoigt: Вот встречный пример flyingfrogblog.blogspot.co.uk/2012/04/…
Джон Харроп

2
Я не вижу, чтобы какой-либо вызов функции вставлялся туда.
Бен Фойгт

13

Несколько примеров из моего опыта:

  • Доступ к инструкциям, которые недоступны из C. Например, многие архитектуры (такие как x86-64, IA-64, DEC Alpha и 64-битный MIPS или PowerPC) поддерживают 64-битное 64-битное умножение, дающее 128-битный результат. GCC недавно добавила расширение, обеспечивающее доступ к таким инструкциям, но до этого требовалась сборка. И доступ к этой инструкции может иметь огромное значение для 64-битных процессоров при реализации чего-то вроде RSA - иногда даже в 4 раза улучшая производительность.

  • Доступ к специфичным для CPU флагам. Тот, кто меня сильно укусил, это флаг для переноски; при выполнении сложения с множественной точностью, если у вас нет доступа к биту переноса ЦП, нужно вместо этого сравнить результат, чтобы увидеть, не переполнился ли он, что требует 3-5 дополнительных инструкций для каждой ветви; и еще хуже, которые являются довольно последовательными с точки зрения доступа к данным, что убивает производительность на современных суперскалярных процессорах. При обработке тысяч таких целых чисел подряд возможность использовать addc является огромным преимуществом (есть и суперскалярные проблемы с конкуренцией за бит переноса, но современные процессоры справляются с этим довольно хорошо).

  • SIMD. Даже автовекторизация компиляторов может выполнять только относительно простые случаи, поэтому, если вам нужна хорошая производительность SIMD, к сожалению, часто необходимо писать код напрямую. Конечно, вы можете использовать встроенные функции вместо ассемблера, но как только вы достигнете уровня встроенных функций, вы все равно в основном пишете сборку, просто используя компилятор в качестве распределителя регистров и (номинально) планировщик команд. (Я склонен использовать встроенные функции для SIMD просто потому, что компилятор может генерировать прологи функций и все такое для меня, поэтому я могу использовать один и тот же код в Linux, OS X и Windows, не имея дело с проблемами ABI, такими как соглашения о вызовах функций, но другие чем то, что встроенные SSE на самом деле не очень хорошие - Altivec кажутся лучше, хотя у меня нет большого опыта с ними).AES или SIMD исправление ошибок: можно представить себе компилятор, который может анализировать алгоритмы и генерировать такой код, но мне кажется, что такой умный компилятор по крайней мере 30 лет от существующего (в лучшем случае).

С другой стороны, многоядерные машины и распределенные системы сместили многие из самых больших выигрышей в производительности в другом направлении - получите дополнительное ускорение на 20% для записи ваших внутренних циклов в сборке, или на 300%, запустив их на нескольких ядрах, или на 10000% на запустить их через кластер машин. И, конечно же, оптимизацию высокого уровня (такие как фьючерсы, запоминание и т. Д.) Часто гораздо проще выполнить на языке более высокого уровня, таком как ML или Scala, чем на C или asm, и зачастую они могут обеспечить гораздо больший выигрыш в производительности. Так что, как всегда, есть компромиссы, которые нужно сделать.


2
@Dennis, поэтому я и написал: «Конечно, вы можете использовать встроенные функции вместо ассемблера, но как только вы попадаете на уровень встроенных функций, вы все равно в основном пишете сборку, просто используя компилятор в качестве распределителя регистров и (номинально) планировщик команд».
Джек Ллойд,

Кроме того, внутренний SIMD-код имеет тенденцию быть менее читабельным, чем тот же код, написанный на ассемблере: большая часть SIMD-кода опирается на неявную реинтерпретацию данных в векторах, что является PITA для встроенных функций компилятора типов данных.
cmaster - восстановить

10

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

http://danbystrom.se/2008/12/22/optimizing-away-ii/

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

Обновлено: О, просто подумайте о том, что является наиболее важным, когда мы говорим о циклах в целом: компилятор часто не имеет ни малейшего представления о том, сколько итераций будет обычным делом! Только программист знает, что цикл будет повторяться много раз и что поэтому будет полезно подготовить цикл с некоторой дополнительной работой, или если он будет повторяться так мало раз, что на самом деле установка займет больше итераций ожидается.


3
Оптимизация по профилю дает компилятору информацию о том, как часто используется цикл.
Zan Lynx

10

Чаще, чем вы думаете, C должен делать вещи, которые кажутся ненужными с точки зрения программиста сборки, только потому, что так говорят стандарты C.

Целочисленное продвижение, например. Если вы хотите сдвинуть переменную char в C, обычно можно ожидать, что код на самом деле сделает только одно - сдвиг в один бит.

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


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

9

Вы на самом деле не знаете, действительно ли ваш хорошо написанный C-код действительно быстр, если вы не смотрели на разборку того, что производит компилятор. Много раз вы смотрите на это и видите, что «хорошо написанное» было субъективным.

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


2
«Так что нет необходимости писать на ассемблере, чтобы получить самый быстрый код». Ну, я не видел, чтобы компилятор делал оптимальную вещь в любом случае, который не был тривиальным. Опытный человек может сделать лучше, чем компилятор практически во всех случаях. Таким образом, абсолютно необходимо писать на ассемблере, чтобы получить «самый быстрый код за всю историю».
cmaster - восстановить

@cmaster По моему опыту вывод компилятора ну, случайный. Иногда это действительно хорошо и оптимально, а иногда - «как мог быть выпущен этот мусор».
Sharp

9

Я прочитал все ответы (более 30) и не нашел простой причины: ассемблер работает быстрее, чем C, если вы прочитали и применили Справочное руководство по оптимизации архитектур Intel® 64 и IA-32 , поэтому причина, по которой сборка может медленнее то, что люди, которые пишут такие медленные сборки, не читали Руководство по оптимизации .

В старые добрые времена Intel 80286 каждая инструкция выполнялась с фиксированным числом циклов ЦП, но после выпуска Pentium Pro, выпущенного в 1995 году, процессоры Intel стали суперскалярными, используя сложную конвейеризацию: выполнение по порядку и переименование регистров. До этого на Pentium, выпущенном в 1993 году, существовали конвейеры U и V: линии с двумя конвейерами, которые могли выполнять две простые инструкции за один такт, если они не зависели друг от друга; но это было не то, что можно сравнить с тем, что «Выполнение вне очереди» и «Переименование регистров» появилось в Pentium Pro и почти не изменилось.

Чтобы объяснить в двух словах, самый быстрый код - это когда инструкции не зависят от предыдущих результатов, например, вы всегда должны очищать целые регистры (с помощью movzx) или использовать add rax, 1вместо них или inc raxудалять зависимость от предыдущего состояния флагов и т. Д.

Вы можете прочитать больше об Оформлении заказа и Переименовании Регистрации, если позволяет время, в Интернете есть много информации.

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

Большинство людей просто не знают о выполнении вне очереди, поэтому они пишут свои программы сборки, например, для 80286, ожидая, что выполнение их инструкции займет фиксированное время независимо от контекста; в то время как компиляторы C знают о выполнении вне очереди и правильно генерируют код. Вот почему код таких незнакомых людей медленнее, но если вы узнаете, ваш код будет быстрее.


8

Я думаю, что общий случай, когда ассемблер работает быстрее, это когда умный программист на ассемблере смотрит на вывод компилятора и говорит: «Это критический путь для производительности, и я могу написать его, чтобы повысить его эффективность», а затем этот человек настраивает ассемблер или переписывает его. с нуля.


7

Все зависит от вашей рабочей нагрузки.

Для повседневных операций C и C ++ просто хороши, но есть определенные рабочие нагрузки (любые преобразования, включающие видео (сжатие, распаковка, эффекты изображения и т. Д.)), Которые в значительной степени требуют сборки для обеспечения производительности.

Они также обычно включают использование специфичных для CPU расширений чипсета (MME / MMX / SSE / что угодно), которые настроены для таких операций.


6

У меня есть операция транспонирования битов, которая должна быть сделана, на 192 или 256 битах на каждое прерывание, что происходит каждые 50 микросекунд.

Это происходит по фиксированной карте (аппаратные ограничения). Используя C, это заняло около 10 микросекунд. Когда я перевел это на Ассемблер, учитывая особенности этой карты, специфическое кэширование регистров и использование бит-ориентированных операций; выполнение заняло менее 3,5 мкс.


6

Возможно, стоит взглянуть на « Оптимизацию неизменяемости и чистоты» Уолтера Брайта. Это не профилированный тест, но он показывает вам один хороший пример различия между рукописным и сгенерированным компилятором ASM. Уолтер Брайт пишет оптимизирующие компиляторы, поэтому, возможно, стоит взглянуть на его другие сообщения в блоге.



5

Простой ответ ... Тот, кто хорошо знает ассемблер (у него также есть ссылка, и он использует все функции кеша, конвейера и т. Д.), Гарантированно способен создавать гораздо более быстрый код, чем любой компилятор.

Однако разница в эти дни просто не имеет значения в типичном приложении.


1
Вы забыли сказать «уделено много времени и сил» и «создать кошмар обслуживания». Мой коллега работал над оптимизацией критически важного для производительности раздела кода ОС, и он работал на C гораздо больше, чем на сборке, поскольку это позволило ему исследовать влияние изменений высокого уровня на производительность в разумные сроки.
Артелиус

Согласен. Иногда вы используете макросы и скрипты для генерации ассемблерного кода, чтобы сэкономить время и быстро развиваться. У большинства ассемблеров в наши дни есть макросы; если нет, вы можете создать (простой) препроцессор макроса, используя (довольно простой RegEx) Perl-скрипт.

Эта. Точно. Компилятор, превосходящий экспертов по доменам, еще не изобретен.
cmaster - восстановить

4

Одной из возможностей версии PolyPascal для CP / M-86 (брат Turbo Pascal) было заменить функцию «использовать биос для вывода символов на экран» процедурой машинного языка, которая по существу были даны x, y и строка для размещения там.

Это позволило обновлять экран намного быстрее, чем раньше!

В двоичном файле было место для встраивания машинного кода (несколько сотен байтов), и там были и другие вещи, поэтому было необходимо сжать как можно больше.

Оказывается, что поскольку экран был размером 80x25, обе координаты могли помещаться в байтах, поэтому обе они могли помещаться в двухбайтовом слове. Это позволило выполнить вычисления, необходимые в меньшем количестве байтов, так как одно добавление может манипулировать обоими значениями одновременно.

Насколько мне известно, нет компиляторов C, которые могут объединять несколько значений в регистр, выполнять SIMD-инструкции для них и разбивать их позже (и я не думаю, что машинные инструкции будут в любом случае короче).


4

Один из наиболее известных фрагментов сборки взят из цикла отображения текстур Майкла Абраша ( подробно описанного здесь ):

add edx,[DeltaVFrac] ; add in dVFrac
sbb ebp,ebp ; store carry
mov [edi],al ; write pixel n
mov al,[esi] ; fetch pixel n+1
add ecx,ebx ; add in dUFrac
adc esi,[4*ebp + UVStepVCarry]; add in steps

В настоящее время большинство компиляторов выражают продвинутые специфичные для процессора инструкции в виде встроенных функций, то есть функций, которые компилируются вплоть до самой инструкции. MS Visual C ++ поддерживает встроенные функции для MMX, SSE, SSE2, SSE3 и SSE4, поэтому вам не нужно беспокоиться о переходе к сборке, чтобы воспользоваться преимуществами инструкций для конкретной платформы. Visual C ++ также может использовать фактическую архитектуру, на которую вы ориентируетесь, с соответствующей настройкой / ARCH.


Более того, эти SSE-компоненты определены Intel, поэтому они на самом деле довольно переносимы.
Джеймс

4

При правильном программировании программы на ассемблере всегда могут быть выполнены быстрее, чем их аналоги на С (по крайней мере, незначительно). Было бы трудно создать C-программу, в которой вы не могли бы вынести хотя бы одну инструкцию Ассемблера.


Это было бы немного более правильным: «Было бы трудно создать нетривиальную программу на C, где ...» В качестве альтернативы вы могли бы сказать: «Было бы трудно найти реальную программу на C, где ...» Точка Существуют тривиальные циклы, для которых компиляторы производят оптимальный вывод. Тем не менее, хороший ответ.
cmaster - восстановить


4

GCC стал широко используемым компилятором. Его оптимизации в целом не так хороши. Гораздо лучше, чем средний программист, пишущий на ассемблере, но для реальной производительности это не так хорошо. Есть компиляторы, которые просто невероятны в коде, который они производят. Так что в качестве общего ответа будет много мест, где вы можете перейти к выводу компилятора и настроить ассемблер для повышения производительности и / или просто переписать подпрограмму с нуля.


8
GCC делает чрезвычайно умные "независимые от платформы" оптимизации. Тем не менее, он не так хорош в использовании конкретных наборов команд в их полном объеме. Для такого переносного компилятора это очень хорошая работа.
Артелиус

2
согласовано. Его мобильность, входящие языки и целевые показатели потрясающие. Быть таким переносимым может и мешает быть действительно хорошим в одном языке или цели. Таким образом, у человека есть возможность добиться большего успеха для конкретной оптимизации по конкретной цели.
old_timer

+1: GCC определенно не конкурентоспособен в создании быстрого кода, но я не уверен, что это потому, что он переносим. LLVM является переносимым, и я видел, что он генерирует код в 4 раза быстрее, чем GCC.
Джон Харроп

Я предпочитаю GCC, так как он уже много лет отлично работает, плюс он доступен практически для любой платформы, на которой может работать современный портативный компилятор. К сожалению, я не смог собрать LLVM (Mac OS X / PPC), поэтому, вероятно, я не смогу перейти на него. Одна из положительных сторон GCC заключается в том, что если вы пишете код, который строится в GCC, вы, скорее всего, придерживаетесь стандартов и будете уверены, что он может быть создан практически для любой платформы.

4

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

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

int y = data [i]; // делать что-то здесь .. call_function (y, ...);

Компилятор будет читать данные, помещать их в стек (разливать), а затем читать из стека и передавать в качестве аргумента. Звучит дерьмо? Это может быть очень эффективная компенсация задержки и привести к более быстрому времени выполнения.

// оптимизированная версия call_function (data [i], ...); // не так оптимизировано в конце концов ..

Идея с оптимизированной версией состояла в том, что мы уменьшили давление в регистре и избежали пролива. Но на самом деле «дерьмовая» версия оказалась быстрее!

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

Здесь нужно обратить внимание: многие эксперты по сборке думают, что знают много, но знают очень мало. Правила меняются от архитектуры к следующей тоже. Например, не существует x86-кода «серебряная пуля», который всегда самый быстрый. В эти дни лучше идти по эмпирическим правилам:

  • память медленная
  • кеш быстрый
  • попробуй лучше кешировать
  • как часто ты будешь скучать? у вас есть стратегия компенсации задержек?
  • Вы можете выполнить 10-100 инструкций ALU / FPU / SSE для одного промаха кэша
  • архитектура приложения важна ..
  • ..но это не помогает, когда проблема не в архитектуре

Кроме того, чрезмерное доверие к компилятору, волшебным образом превращающее плохо продуманный код C / C ++ в «теоретически оптимальный» код, является желанным мышлением. Вы должны знать компилятор и цепочку инструментов, которые вы используете, если вы заботитесь о «производительности» на этом низком уровне.

Компиляторы в C / C ++, как правило, не очень хороши в переупорядочении подвыражений, потому что функции имеют побочные эффекты, для начала. Функциональные языки не страдают от этого предостережения, но не соответствуют нынешней экосистеме. Существуют опции компилятора, которые позволяют смягчить правила точности, которые позволяют компилятору / компоновщику / генератору кода изменять порядок операций.

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.