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

441

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

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

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

  • 0
    А теперь уместен другой вопрос: когда на самом деле имеет значение тот факт, что ассемблер быстрее, чем C?
  • 0
    Один из величайших вопросов, которые я видел. Спасибо, Адам!
Показать ещё 8 комментариев
Теги:
performance
assembly

38 ответов

232
Лучший ответ

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

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


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


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
}

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

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

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

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

В дополнение к этому: использование 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 могли бы сделать это без встроенных функций.)

См. Источник + asm для x86 (32-бит) и ARM > 16)%3B+//shift+by the fixed point+bias } //Modern+Compilers know that 32-bit integers+Cast to 64 //still only have 32+significant+bits, //so one 32-bit signed multiply is sufficient #ifdef _MSC_VER %23include+ //static inline int FixedPointMul_msvc (int a, int+b) {%0A+ return (int)+__ll_rshift(__emul(a,b),16); } #endif /* Intrinsics are more useful for+extended precision%0A+* when there isn!'t a wide-enough type.%0A+*+e.g. 128-bit integer on+Compilers without __int128%0A+*/ '),l:'5',n:'0',o:'C++ source #1',t:'0')),k:32.75251522372254,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((g:!((h:compiler,i:(compiler:g412,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'1',trim:'1'),lang:c++,libs:!(),options:'-xc -O3 -m32+ -fomit-frame-pointer',source:1),l:'5',n:'0',o:'x86-64 gcc 4.1.2+(Editor+#1,+Compiler+#1)+C++',t:'0')),k:34.10775747948107,l:'4',m:50,n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:arm710,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',trim:'1'),lang:c++,libs:!(),options:'-xc -O3 -mthumb -mcpu=cortex-m4',source:1),l:'5',n:'0',o:'ARM gcc 7.2.1+(none) (Editor+#1,+Compiler+#2)+C++',t:'0')),header:(),l:'4',m:50,n:'0',o:'',s:0,t:'0')),k:33.91415144294414,l:'3',n:'0',o:'',t:'0'),(g:!((g:!((h:compiler,i:(compiler:clang30,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',trim:'1'),lang:c++,libs:!(),options:'-xc -O3 -m32',source:1),l:'5',n:'0',o:'x86-64+Clang 3.0.0+(Editor+#1,+Compiler+#3)+C++',t:'0')),k:33.33333333333333,l:'4',m:50,n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:cl19_2015_u3_32,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',trim:'1'),lang:c++,libs:!(),options:'-Ox',source:1),l:'5',n:'0',o:'x86 MSVC 19 2015 U3 (Editor+#1,+Compiler+#4)+C++',t:'0')),header:(),l:'4',m:50,n:'0',o:'',s:0,t:'0')),k:33.33333333333333,l:'3',n:'0',o:'',t:'0')),l:'2',n:'0',o:'',t:'0')),version:4 rel=noreferrer>в проводнике компилятора Godbolt. (К сожалению, у него нет каких-либо компиляторов, достаточно старых, чтобы создать плохой код из простой версии C).


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

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

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

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


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

  • 0
    Я давно знал об этом.
  • 4
    Как насчет таких вещей, как {x = c% d; y = c / d;}, достаточно ли умны компиляторы, чтобы сделать это одним div или idiv?
Показать ещё 10 комментариев
123

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

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

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

  • 3
    Просто интересно: была ли картинка в формате 1 бит на пиксель?
  • 3
    Да, это была одноразрядная монохромная система, в частности это были монохромные блоки изображений на Atari ST.
Показать ещё 4 комментария
57

В любом случае, когда компилятор видит код с плавающей точкой, ручная версия будет быстрее. Основная причина заключается в том, что компилятор не может выполнять никаких надежных оптимизаций. См. эту статью из 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, и это не имело никакого значения для таймингов - иногда быстрее, иногда медленнее. Я предполагаю, что ограниченный объем памяти увеличивает другие оптимизации.

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

  • 0
    +1 за выполнение профилирования, но было бы неплохо включить вывод в свой ответ.
  • 0
    К вашему сведению: код может быть даже быстрее, если вы замените цикл на sub ecx, 1 / bnz l1. Цикл намного медленнее, чем мог бы быть (по какой-то причине, но это другая тема).
Показать ещё 8 комментариев
52

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

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

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

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

  • 0
    Как правило, это утверждение верно. Компилятор делает все возможное для DWIW, но в некоторых крайних случаях ассемблер ручного кодирования выполняет свою работу, когда производительность в реальном времени является обязательной.
  • 1
    @Liedman: «он может попытаться изменить порядок команд быстрее, чем человек». OCaml известен своей быстротой, и, что удивительно, его компилятор с ocamlopt кодом ocamlopt пропускает планирование команд на x86 и вместо этого оставляет его на ЦП, потому что он может более эффективно переупорядочивать во время выполнения.
Показать ещё 1 комментарий
42

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

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

p >

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

  • 0
    Блин ... пропустил тот;) Исправлено ("бранч" -> "ветвление"). На более серьезном уровне я бы также сказал, что вы можете ожидать как минимум на порядок большей производительности.
  • 0
    @Lieven: вы едите суп только на ужин?
Показать ещё 7 комментариев
41

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

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

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

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

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

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

  • 0
    Вы не укладываете плитки? :-)
  • 0
    @plinth: как вы понимаете, "циклы соскабливания"?
Показать ещё 3 комментария
39

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

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

    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 умножить и делать правильные вещи с машиной 16x16multiply. Или это может быть глупо и требует вызова библиотеки, чтобы сделать 32x32 размножение таким образом излишним, потому что вам нужно только 16 бит продукта, но стандарт C не дает вам никакого способа выразить себя.

  • Некоторые операции с битрейтом (вращение/перенос):

    // 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 8- бит с правым сдвигом, используя автоинкремент на указателе.

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

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

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

22

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

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

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

Это смешно, потому что это правда: 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, заключалось в том, что кто-то мог написать свою библиотеку, чтобы он был оптимизирован для любого процессора, над которым вы работали. Это была тяжелая работа в ассемблере.

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

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

  • 0
    Было много людей, которые использовали ассемблер ARM в качестве языка выбора на машинах Acorn (начало 90-х). IIRC сказали, что небольшой набор инструкций по рискам делает его более легким и увлекательным. Но я подозреваю, что это потому, что компилятор C опоздал на Acorn, а компилятор C ++ так и не был завершен.
  • 3
    «... потому что нет субъективного стандарта для C.» Вы имеете в виду цель .
Показать ещё 2 комментария
15

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

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

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

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

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

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

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

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

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

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

Дэвид.

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

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

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

Это еще одна причина, по которой команда NOP (No Operation - do nothing) ничего не делает) может фактически заставить все ваше приложение работать быстрее.

[EDIT] Конечно, этот метод зависит от конкретной аппаратной настройки. Это была основная причина, по которой многие игры Amiga не могли справиться с более быстрыми процессорами: время выполнения инструкций было отключено.

  • 0
    В Amiga не было 16 МБ оперативной памяти, больше от 512 до 2 МБ в зависимости от чипсета. Кроме того, многие игры Amiga не работают с более быстрыми процессорами из-за описанных вами методов.
  • 1
    @ bk1e - Amiga произвела большое количество различных моделей компьютеров, в моем случае Amiga 500 поставлялась с оперативной памятью 512K, увеличенной до 1Meg. amigahistory.co.uk/amiedevsys.html является амигой с 128Meg Ram
Показать ещё 4 комментария
14

Я удивлен, что никто этого не сказал. Функция 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 ?
  • 0
    @ninjalj: это одно и то же :) я не думал, что это можно сделать таким образом в C. Это может быть немного улучшено, я думаю
Показать ещё 3 комментария
13

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

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

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

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

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

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

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

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

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

  • 7
    +1: «Вы можете отклониться от условностей вызова». Компиляторы C / C ++ имеют тенденцию отстой при возврате нескольких значений. Они часто используют форму sret, где стек вызывающей стороны выделяет непрерывный блок для структуры и передает ссылку на нее вызываемому объекту, чтобы заполнить его. Возвращение нескольких значений в регистрах происходит в несколько раз быстрее.
  • 0
    @Jon: компиляторы C / C ++ прекрасно справляются с этой задачей, когда функция становится встроенной (не встроенные функции должны соответствовать ABI, это не ограничение C и C ++, а модель связывания)
Показать ещё 2 комментария
12

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

  • 0
    Некоторые компиляторы (VectorC, если я правильно помню) генерируют SIMD-код, так что даже это, вероятно, больше не является аргументом для использования ассемблерного кода.
  • 0
    Компиляторы создают код с поддержкой SSE, поэтому этот аргумент неверен
Показать ещё 7 комментариев
10

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

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

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

10

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

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

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

  • SIMD

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

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

  • 0
    Встроенные функции компилятора SIMD доступны из кода C / C ++ ...
  • 2
    @Dennis, поэтому я и написал: «Конечно, вы можете использовать встроенные функции вместо ассемблера, но как только вы попадаете на уровень встроенных функций, вы все равно пишете сборку, просто используя компилятор в качестве распределителя регистров и (номинально) планировщик команд»
Показать ещё 1 комментарий
10

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

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

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

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

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

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

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

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

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

7

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

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

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

6

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

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

5

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

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

  • 1
    Вы забыли сказать «уделено много времени и сил» и «создать кошмар обслуживания». Мой коллега работал над оптимизацией критически важного для производительности раздела кода ОС, и он работал на C гораздо больше, чем на сборке, поскольку это позволило ему исследовать влияние изменений высокого уровня на производительность в разумные сроки.
  • 0
    Согласен. Иногда вы используете макросы и скрипты для генерации ассемблерного кода, чтобы сэкономить время и быстро развиваться. У большинства ассемблеров в наши дни есть макросы; если нет, вы можете создать (простой) препроцессор макроса, используя (довольно простой RegEx) Perl-скрипт.
Показать ещё 1 комментарий
5

LInux assembly howto, задает этот вопрос и дает плюсы и минусы использования сборки.

4

Я прочитал все ответы (более 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 знают о выполнении вне очереди и правильно генерируют код. Вот почему код таких незнакомых людей медленнее, но если вы узнаете, ваш код будет быстрее.

4

Как насчет создания машинного кода во время выполнения?

Мой брат однажды (около 2000) реализовал чрезвычайно быстрый трассировщик лучей в реальном времени, создав код во время выполнения. Я не могу вспомнить детали, но был какой-то главный модуль, который перебирал объекты, затем он готовил и выполнял некоторый машинный код, который был специфичен для каждого объекта.

Однако со временем этот метод был отменен новым графическим оборудованием, и он стал бесполезным.

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

  • 0
    «Создание машинного кода во время выполнения»: это всего лишь метод оценки. Это также известно как гром . Широкое использование thunks может упростить некоторые методы повышения производительности, но обычно это не является их главной целью, так же как вы не используете объектно-ориентированное программирование специально для решения проблем производительности.
4

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

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

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

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

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

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

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

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

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

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

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

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

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

4

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

  • 0
    -1, весьма субъективный комментарий ..
  • 8
    GCC делает чрезвычайно умные "независимые от платформы" оптимизации. Тем не менее, он не так хорош в использовании конкретных наборов команд в их полном объеме. Для такого переносного компилятора это очень хорошая работа.
Показать ещё 3 комментария
4

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

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

http://cr.yp.to/qhasm.html имеет много примеров.

4

Возможно, стоит обратить внимание на Optimizing Immutable and Purity от Walter Bright, это не профилированный тест, но показывает вам один хороший пример разницы между рукописным и созданным компилятором ASM. Уолтер Брайт пишет оптимизирующие компиляторы, поэтому, возможно, стоит посмотреть на его другие сообщения в блоге.

  • 0
    эта ссылка мертва
  • 1
    @Quonux лучше поздно, чем никогда, я обновил ссылку
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 С++ поддерживает встроенные функции для MMX, SSE, SSE2, SSE3 и SSE4, поэтому вам нужно меньше беспокоиться о том, чтобы отказаться от сборки, чтобы воспользоваться инструкциями конкретной платформы. Visual С++ также может использовать фактическую архитектуру, которую вы нацеливаете, с соответствующей настройкой/ARCH.

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

Одна из возможностей для версии PolyPascal CP/M-86 (sibling to Turbo Pascal) заключалась в замене объекта "use-bios-to-output-characters-to-the-screen" с помощью процедуры машинного языка который в сущности был задан x, и y, и строку, которую нужно положить туда.

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

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

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

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

3

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

1

На самом деле вы можете создавать крупномасштабные программы в режиме большой модели. Сегунги могут быть ограничены кодом 64 КБ, но вы можете писать много сегментов, люди дают аргумент против ASM, поскольку это старый язык, и нам не нужно сохранять память Если бы это было так, то почему бы нам упаковать наш компьютер с памятью, единственный недостаток, который я могу найти в ASM, заключается в том, что он более или менее основан на процессоре, поэтому большинство программ, написанных для архитектуры Intel, скорее всего, не будут работать на AMD Архитектура. Что касается C, то быстрее, чем ASM, язык быстрее, чем ASM, и ASM может делать много вещей C, а другие HLL не могут выполнять на уровне процессора. ASM - сложный язык для изучения, но как только вы его узнаете, HLL не сможет перевести его лучше, чем вы. Если бы вы могли видеть только некоторые из вещей, которые HLL делает вам для кода и понимаете, что он делает, вы бы задались вопросом, почему больше людей не используют ASM и почему сборщики больше не обновляются (для общего пользования в любом случае). Таким образом, C не быстрее ASM. Даже опыты программистов на C++ все еще используют и пишут куски кода в ASM, добавленные там код С++ для скорости. Другие языки Кроме того, некоторые люди считают устаревшими или, возможно, нехорошо, это миф порой, например, Photoshop написан на Pascal/ASM. 1-й выпуск souce был отправлен в технический музей истории, а paintshop pro написан еще на Python, TCL и ASM... общий знаменатель этих "быстрых и совершенных обработчиков изображений" - это ASM, хотя Photoshop может быть обновлен до delphi, теперь он по-прежнему является pascal, и любые проблемы скорости происходят от pascal, но это потому, что нам нравится путь программы выглядят, а не то, что они делают сейчас. Я хотел бы сделать Photoshop Clone в чистом ASM, над которым я работал, и он работает хорошо, а не код, интерпретация, изменение, переименование и т.д. Просто код и завершить процесс.

1

В те времена, когда скорость процессора измерялась в МГц, а размер экрана был ниже 1 мегапикселя, хорошо известным трюком для более быстрого отображения было разворачивание циклов: запись операции для каждой строки сканирования на экране. Он избегал накладных расходов на поддержание индекса цикла! В сочетании с обнаружением обновления экрана это было довольно эффективно.
То, что компилятор C не будет делать... (хотя часто вы можете выбирать между оптимизацией для скорости или размера, я полагаю, что первый использует некоторые подобные трюки.)

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

  • 6
    Развертывание цикла является стандартным для современных компиляторов.
  • 1
    Текущий gcc не разворачивается по умолчанию на x86, кроме как с помощью профильной оптимизации. Его циклы часто являются узким местом на внешнем интерфейсе, но большинство циклов запускаются недостаточно часто, чтобы оправдать затраты на развертывание кода. А без PGO gcc не знает, какие крошечные петли горячие.
Показать ещё 1 комментарий
1

Это очень сложно ответить конкретно, потому что вопрос очень неспецифичен: что такое "современный компилятор"?

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

  • 0
    Для целей этого вопроса «современный» компилятор просто «лучший компилятор, доступный для работы». Случаи, когда ассемблер лучше просто из-за плохого выбора компилятора, не должны учитываться. Поэтому я прошу конкретные примеры: вы используете лучший доступный компилятор, но asm все еще лучше.
  • 0
    Очевидно, что «лучший» субъективен, но он, по крайней мере, не должен быть настолько плохим выбором, чтобы кто-то мог сказать «Да, но если бы вы использовали такой-н-такой компилятор, он бы сам сделал это».
Показать ещё 1 комментарий
0

В настоящее время, учитывая такие компиляторы, как Intel С++, которые чрезвычайно оптимизируют код C, очень сложно конкурировать с выходом компиляторов.

  • 6
    Это не совсем так. Если вы начали программировать на ассемблере и можете читать его так же быстро, как C, вы будете потрясены выходом GCC и других компиляторов. На ПК (так как они так быстры в настоящее время), это обычно не имеет значения. Но когда вы возитесь с видеоизображениями, это имеет огромное значение.
  • 1
    Если компилятор C может оптимизировать ваш код, то опытный программист на ассемблере может сделать лучше. Разве это не очевидно?
0

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

  • 0
    Нет, этого недостаточно. Вы также должны быть готовы отказаться от способности компилятора оптимизировать большие объемы кода. Изменение где-то одной константы может привести к появлению различных интересных трюков asm во многих местах, где функция встроена, и если вы не собираетесь делать это вручную для каждого изменения источника, это того не стоит. (Если ваш компилятор не очень плохой, и вы можете сильно его побить.)
  • 0
    Вы также должны отказаться от возможности будущего перекомпилировать с лучшим компилятором через 10 или 20 лет, чтобы ваш код был оптимизирован для любой микроархитектуры, которая актуальна тогда. (Даже если это все еще x86, другие варианты инструкций могут быть лучше.) Ваш рукописный код будет оптимизирован для Haswell или Skylake и, возможно, Ryzen, если вы это имели в виду при написании, но вы не можете знать, какие инструкции будут быть медленнее или быстрее через 20 лет. TL: DR: написать C, который лучше оптимизирует, если это возможно, вместо того, чтобы писать asm
-2

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

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

  • 4
    Заявление о «крайних случаях» говорит мне, что вы не пишете много ассемблера. Я всегда могу выбить штаны из компилятора Си, используя ассемблер. Единственная проблема - решить, стоит ли это того или иное.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню