Почему uint_least16_t быстрее, чем uint_fast16_t для умножения в x86_64? - PullRequest
11 голосов
/ 07 ноября 2010

Стандарт C совершенно неясен для семейства типов uint_fast*_t.В системе Linux x86_64 gcc-4.4.4 оба типа uint_fast16_t и uint_fast32_t имеют размер 8 байтов.Тем не менее, умножение 8-байтовых чисел кажется довольно медленным, чем умножение 4-байтовых чисел.Следующий фрагмент кода демонстрирует, что:

#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

int
main ()
{
  uint_least16_t p, x;
  int count;

  p = 1;
  for (count = 100000; count != 0; --count)
    for (x = 1; x != 50000; ++x)
      p*= x;

  printf("%"PRIuLEAST16, p);
  return 0;
}

При запуске команды времени в программе я получаю

real 0m7.606s
user 0m7.557s
sys  0m0.019s

Если я изменю тип на uint_fast16_t (и модификатор printf) время становится равным

real 0m12.609s
user 0m12.593s
sys  0m0.009s

Итак, не будет ли намного лучше, если бы заголовок stdint.h определил uint_fast16_t (а также uint_fast32_t) как 4-байтовый тип?

Ответы [ 5 ]

5 голосов
/ 28 августа 2012

Компиляторы AFAIK определяют свои собственные версии типов (u)int_(fast/least)XX_t, только если они еще не определены системой. Это потому, что очень важно, чтобы эти типы были одинаково определены во всех библиотеках / двоичных файлах в одной системе. В противном случае, если разные компиляторы будут определять эти типы по-разному, библиотека, созданная с помощью CompilerA, может иметь тип uint_fast32_t, отличный от двоичного файла, созданного с помощью CompilerB, но этот двоичный файл все еще может ссылаться на библиотеку; не существует формального стандартного требования о том, что весь исполняемый код системы должен быть собран одним и тем же компилятором (фактически, в некоторых системах, например, в Windows, довольно распространено, что код был скомпилирован всеми видами различных компиляторы). Если теперь этот двоичный файл вызывает функцию библиотеки, вещи сломаются!

Итак, вопрос в том, действительно ли GCC определяет здесь uint_fast16_t, или это на самом деле Linux (я имею в виду ядро ​​здесь) или, может быть, даже Standard C Lib (в большинстве случаев glibc), который определяет эти типы? Поскольку, если Linux или glibc определяют их, GCC, построенный на этой системе, не имеет другого выбора, кроме как принять те соглашения, которые они установили. То же самое верно и для всех других типов переменной ширины: char, short, int, long, long long; все эти типы имеют только минимальную гарантированную битовую ширину в C Standard (а для int это на самом деле 16 бит, поэтому на платформах, где int 32-битный, он уже намного больше, чем по требованию стандарта).


Кроме этого, мне действительно интересно, что не так с вашим CPU / компилятором / системой. В моей системе 64-битное умножение одинаково быстро для 32-битного умножения. Я изменил ваш код для тестирования 16, 32 и 64 бит:

#include <time.h>
#include <stdio.h>
#include <inttypes.h>

#define RUNS 100000

#define TEST(type)                                  \
    static type test ## type ()                     \
    {                                               \
        int count;                                  \
        type p, x;                                  \
                                                    \
        p = 1;                                      \
        for (count = RUNS; count != 0; count--) {   \
            for (x = 1; x != 50000; x++) {          \
                p *= x;                             \
            }                                       \
        }                                           \
        return p;                                   \
    }

TEST(uint16_t)
TEST(uint32_t)
TEST(uint64_t)

#define CLOCK_TO_SEC(clock) ((double)clockTime / CLOCKS_PER_SEC)

#define RUN_TEST(type)                             \
    {                                              \
        clock_t clockTime;                         \
        unsigned long long result;                 \
                                                   \
        clockTime = clock();                       \
        result = test ## type ();                  \
        clockTime = clock() - clockTime;           \
        printf("Test %s took %2.4f s. (%llu)\n",   \
            #type, CLOCK_TO_SEC(clockTime), result \
        );                                         \
    }

int main ()
{
    RUN_TEST(uint16_t)
    RUN_TEST(uint32_t)
    RUN_TEST(uint64_t)
    return 0;
}

Используя неоптимизированный код (-O0), я получаю:

Test uint16_t took 13.6286 s. (0)
Test uint32_t took 12.5881 s. (0)
Test uint64_t took 12.6006 s. (0)

Используя оптимизированный код (-O3), я получаю:

Test uint16_t took 13.6385 s. (0)
Test uint32_t took 4.5455 s. (0)
Test uint64_t took 4.5382 s. (0)

Второй вывод довольно интересный. @R .. написал в комментарии выше:

В x86_64 32-битная арифметика никогда не должна быть медленнее 64-битной арифметика, точка.

Второй вывод показывает, что то же самое нельзя сказать о 32/16-битной арифметике. 16-разрядная арифметика может быть значительно медленнее на 32/64-разрядном процессоре, хотя мой процессор x86 может выполнять 16-разрядную арифметику изначально; в отличие от некоторых других процессоров, таких как, например, PPC, которые могут выполнять только 32-битную арифметику. Однако, похоже, что это применимо только к умножению на моем ЦП, когда при изменении кода на сложение / вычитание / деление больше не будет существенной разницы между 16 и 32 битами.

Приведенные выше результаты получены с Intel Core i7 (2,66 ГГц), но, если кому-то интересно, я могу запустить этот тест также на Intel Core 2 Duo (на одно поколение процессора старше) и на Motorola PowerPC G4.

3 голосов
/ 15 мая 2013

Фактическая производительность во время выполнения - очень и очень сложная тема.Со многими факторами, начиная от оперативной памяти, жестких дисков, ОС;И множество специфических особенностей процессора.Но это даст вам грубое сокращение:

N_fastX_t

  • Оптимальный размер для эффективного вычисления большинства операций (сложение и вычитание) для процессора.Это зависит от аппаратного обеспечения, в котором 32-битные переменные являются родными и более быстрыми (и, следовательно, используются) по сравнению с 16-битными переменными.(например)
  • Поскольку это не приносит пользы так же, как N_leastX, с точки зрения попаданий в кэш-строки, это следует использовать в основном, когда требуется только одна переменная как можно быстрее.Несмотря на то, что он не находится в большом массиве (насколько велико оптимальное переключение между ними, это, к сожалению, зависит от платформы)
  • Обратите внимание, что быстрое против наименьшего имеет несколько причуд, в основном это умножение и деление.Это зависит от платформы.Однако, если большинство операций являются дополнениями / подстановками / или / и.Обычно безопасно предполагать, что быстрее быстрее.(еще раз обратите внимание на кэш процессора и другие особенности)

N_leastX_t

  • Наименьшая переменная, допустимая аппаратным обеспечением, то есть, по крайней мере,Размер X(некоторые платформы не могут назначить переменные ниже 4 байтов, например. Фактически большинство ваших переменных BOOL занимают, по крайней мере, байт, а не бит) *
  • Может быть рассчитано с помощью дорогостоящей программной эмуляции ЦП, если аппаратная поддержкане существует.
  • Может иметь снижение производительности из-за частичной аппаратной поддержки (по сравнению с быстрой) в одной операции.
  • HOWEVER : так как она требует меньше переменнойпространство, это может чаще попадать в строки кэша.Это гораздо более заметно в массивах.И как таковой будет быстрее (стоимость памяти> стоимость процессора) Подробнее см. http://en.wikipedia.org/wiki/CPU_cache.

Проблема умножения?

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

http://en.wikipedia.org/wiki/Binary_multiplier

//Assuming 4bit int
   0011 (3 in decimal)
 x 0101 (5 in decimal)
 ======
   0011 ("0011 x 0001")
  0000- ("0011 x 0000")
 0011-- ("0011 x 0001")
0000--- ("0011 x 0000")
=======
   1111 (15 in decimal)

Однако важно знать, что компьютер - «логический идиот».В то время как для нас, людей, очевидно, пропустить завершающий шаг нулей.Компьютер все равно сработает (дешевле, чем проверять, а потом все равно проверять).Следовательно, это создает причуду для переменной большего размера с тем же значением

   //Assuming 8bit int
      0000 0011 (3 in decimal)
    x 0000 0101 (5 in decimal)
    ===========
      0000 0011 ("0011 x 0001")
    0 0000 000- ("0011 x 0000")
   00 0000 11-- ("0011 x 0001")
  000 0000 0--- ("0011 x 0000")
 0000 0000 ---- (And the remainders of zeros)
 -------------- (Will all be worked out)
 ==============
      0000 1111 (15 in decimal)

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

Однако здесь возникает вторая причуда.Это может относиться не ко всем процессорам.Важно отметить, что все операции процессора учитываются в циклах процессора.В котором в каждом цикле выполняются десятки (или более) таких небольших операций сложения, как показано выше.В результате 8-битное сложение может занять столько же времени, сколько и 8-битное умножение и т. Д. Из-за различных оптимизаций и специфических особенностей процессора.

Если это вас так сильно волнует.Обратитесь к Intel: http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html


Дополнительное упоминание о ЦП и ОЗУ

Поскольку ЦП продвигаются по закону Мура, чтобы быть в несколько раз быстрее, чем ваш DDR3БАРАН.

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

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

3 голосов
/ 07 ноября 2010

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

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

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

Теперь вернемся к вашему примеру. Я полагаю, вы также посмотрели код ассемблера? Например, он использовал инструкции SSE для реализации вашего кода? Вы включили опции, специфичные для процессора, что-то вроде -march=native?

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

Например, если я изменю внутренний цикл и на нисходящий, ассемблер будет выглядеть почти так же, как и раньше (но с использованием декремента и проверки на 0), но выполнение займет примерно на 50% больше. Так что я думаю, что время очень сильно зависит от среды команды, которую вы хотите сравнить, конвейерная остановка, что угодно. Вы должны были бы сравнить коды совсем другого характера, когда инструкции выпускаются в разных контекстах, возникают проблемы с выравниванием и векторизацией, чтобы принять решение, какие типы fast typedef являются подходящими.

2 голосов
/ 07 ноября 2010

Да, я думаю, что это просто ошибка. К сожалению, вы не можете просто исправлять подобные ошибки, не нарушая ABI, но это может не иметь значения, поскольку практически никто (и, конечно, никакие библиотечные функции, о которых я знаю) фактически не использует типы *int_fast*_t.

1 голос
/ 06 февраля 2015

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

На уровне сборки существует 8 случаев: четыре для размеров и 2 для подписи.24 имени типа ISO C должны быть сопоставлены с восемью основными типами.Как уже говорил Дженс, «хорошее» отображение должно учитывать конкретный процессор и конкретный код.Поэтому на практике не следует ожидать идеальных результатов, даже если создатели компилятора должны знать сгенерированный код.

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

  • Нет разницы во времени выполнения между int16_t / uint16_t и int64_t / uint64_t соответственно.
  • Без подписи версии намного быстрее дляint8_t / uint8_t и int32_t / uint32_t соответственно.
  • Беззнаковые версии всегда меньше (текст и сегмент данных), чем подписанные версии.

Компилятор: g ++4.9.1, Опции: -O3 mtune = универсальный -march = x86-64

Процессор: Intel ™ Core ™ 2 Duo E8400 @ 3,00 ГГц

Отображение

|    |Integer|                                                                     |
|Sign|Size   | Types                                                               |
|    |[bits] |                                                                     |
|:--:|------:|:-------------------------------------------------------------------:|
| u  |   8   | uint8_t  uint_fast8_t   uint_least8_t                               |
| s  |   8   | int8_t   int_fast8_t    int_least8_t                                |
| u  |  16   | uint16_t uint_least16_t                                             |
| s  |  16   | int16_t  int_least16_t                                              |
| u  |  32   | uint32_t uint_least32_t                                             |
| s  |  32   | int32_t  int_least32_t                                              |
| u  |  64   | uint64_t uint_fast16_t  uint_fast32_t  uint_fast64_t uint_least64_t |
| s  |  64   | int64_t  int_fast16_t   int_fast32_t   int_fast64_t  int_least64_t  |

Размеры и сроки

|      | Integer |         |       |       |         |
| Sign | Size    |  text   |  data |  bss  | Time    |
|      | [bits]  | [bytes] |[bytes]|[bytes]| [ms]    |
|:----:|--------:|--------:| -----:|------:|--------:|
|  u   |    8    | 1285801 |  3024 |  5704 | 407.61  |
|  s   |    8    | 1285929 |  3032 |  5704 | 412.39  |
|  u   |   16    | 1285833 |  3024 |  5704 | 408.81  |
|  s   |   16    | 1286105 |  3040 |  5704 | 408.80  |
|  u   |   32    | 1285609 |  3024 |  5704 | 406.78  |
|  s   |   32    | 1285921 |  3032 |  5704 | 413.30  |
|  u   |   64    | 1285557 |  3032 |  5704 | 410.12  |
|  s   |   64    | 1285824 |  3048 |  5704 | 410.13  |
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...