Подтвердить что ты не робот

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

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

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

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

4b9b3361

Ответ 1

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

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

Ответ 2

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

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

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

Ответ 3

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

Ответ 4

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

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

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

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

Ответ 5

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

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

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

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

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

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

Ответ 6

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

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

p >

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

Ответ 7

Хотя 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), это упрощает некоторые алгоритмы.

Ответ 8

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

Технически каждая абстракция имеет стоимость, а язык программирования - это абстракция того, как работает ЦП. 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 имеет тенденцию доминировать там тоже) и так далее. Или просто для ударов (что несколько мазохистски).

Ответ 9

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

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

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

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

Ответ 10

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

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

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

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

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

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

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

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

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

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

Дэвид.

Ответ 11

Я удивлен, что никто этого не сказал. Функция 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!

Ответ 12

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

Ответ 13

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

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

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

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

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

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

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

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

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

Ответ 14

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

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

Ответ 15

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

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

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

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

Ответ 16

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

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

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

Ответ 17

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

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

Ответ 18

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

Ответ 19

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

Ответ 20

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

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

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

Ответ 21

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

Ответ 22

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

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

Ответ 23

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

Ответ 24

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

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

Ответ 25

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

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

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

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

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

Ответ 26

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

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.

Ответ 27

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

Ответ 29

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

Ответ 30

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

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

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

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

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

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

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

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

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

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

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

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

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