От наносекунд до миллисекунд - быстрое деление на 1000000

Я хочу преобразовать вывод из gethrtime в миллисекунды.

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

Существует ли оптимизированная операция деления при работе с цифрами 1000000?

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

Некоторые быстрые тесты с использованием кода ниже... надеюсь, что это правильно.

#include <sys/time.h>
#include <iostream>

using namespace std;

const double NANOSECONDS_TO_MILLISECONDS = 1.0 / 1000000.0;

int main()
{
    hrtime_t start;
    hrtime_t tmp;
    hrtime_t fin;

    start = gethrtime();
    tmp = (hrtime_t)(start * NANOSECONDS_TO_MILLISECONDS);
    fin = gethrtime();

    cout << "Method 1"
    cout << "Original val: " << start << endl;
    cout << "Computed: " << tmp << endl;
    cout << "Time:" << fin - start << endl;

    start = gethrtime();
    tmp = (start / 1000000);
    fin = gethrtime();

    cout "Method 2"    
    cout << "Original val: " << start << endl;
    cout << "Computed: " << tmp << endl;
    cout << "Time:" << fin - start << endl;

    return 0;
}  

Пример выходов:

Original val: 3048161553965997
Computed: 3048161553
Time:82082
Original val: 3048161556359586
Computed: 3048161556
Time:31230

Original val: 3048239663018915
Computed: 3048239663
Time:79381
Original val: 3048239665393873
Computed: 3048239665
Time:31321

Original val: 3048249874282285
Computed: 3048249874
Time:81812
Original val: 3048249876664084
Computed: 3048249876
Time:34830

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

Ответ 1

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

Ответ 2

Пусть ваш компилятор это выяснит!

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

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

unsigned long div1000000(unsigned long n) {
  return n / 1000000UL;
}

Скомпилированный с gcc 4.3.3 на x86 (-O3, -fomit-frame-pointer), я получаю:

$ objdump -d div.o -M intel

test2.o:     file format elf32-i386


Disassembly of section .text:

00000000 <div1000000>:
   0:   b8 83 de 1b 43          mov    eax,0x431bde83
   5:   f7 64 24 04             mul    DWORD PTR [esp+0x4]
   9:   c1 ea 12                shr    edx,0x12
   c:   89 d0                   mov    eax,edx
   e:   c3                      ret    

Другими словами, компилятор взял n / 1000000UL и превратил его в (unsigned long long)(n * 0x431bde83) >> (0x12 + 32). Почему это работает? С головы до ног, я понятия не имею! Но компилятор решил, что это будет быстрее, чем выдача собственного разрыва.

Мораль истории:

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

Ответ 3

Я удивлен, что никто еще не получил этого...

  • деление такое же, как умножение на долю
  • умножение на дробную степень 2 быстро: просто бит-сдвиг
  • интегральное деление включает округление
  • округление похоже на умножение на немного меньшую фракцию (до определенной точки, вам нужно знать ваши диапазоны)

Итак,

const uint64_t numerator = (1LL<<32)/1000000;

...

millionths = ( number * numerator ) >> 32;

Супа быстро!

Ответ 4

Умножьте на 1/1 000 000. Это должно быть быстрее. Мой поисковый запрос Google ускорял деления, умножаясь на взаимные. Поэтому я бы предварительно вычислил обратный или список обратных, если существует относительно известный набор возможных значений, а затем умножьте.

Jacob

Ответ 5

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

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

Если, (и только если) это ваше узкое место, то работайте над его улучшением.

Теперь, на ваши варианты улучшения:

1. Вам может не понадобиться мгновенно конвертировать в миллисекунды. Если вы просто собираете данные, просто сохраните полное 64-битное число, возвращенное из gethrtime(), и сделайте с ним. Все, что человек должен читать, может быть подвергнут последующей обработке или на гораздо менее агрессивной частоте обновления.

2. Если вы отсчитываете какое-то повторяющееся событие, вы можете попробовать выполнить деление на разницу между двумя вызовами, которая должна быть очень мала, если вы часто вызываете gethrtime(), чтобы иметь узкое место:

static hrtime_t oldtime;
hrtime_t newtime = gethrtime();
int milliseconds = fastDivByOneMillion((UI32)(newtime - oldtime));
oldtime = newtime;

3. Вы можете реализовать fastDivByOneMillion() как умножение и деление на мощность 2:

int fastDivByOneMillion(UI32 nanoseconds)
{
    return (int)((UI64)nanoseconds * 4295 >> 32);
}

Примечания:

Ваш компилятор может найти лучший способ сделать >> 32 на вашем оборудовании. Большую часть времени это будет только один или два такта. Я использовал UI32 и UI64 для представления 32 и 64-разрядных чисел без знака. Все это потребует больше профилирования, чтобы быть уверенным в том, что оно действительно производит измеримое улучшение.

Ответ 6

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

Во-вторых, насколько точным должен быть результат? Удобное эмпирическое правило для преобразования между двоичным и десятичным состоит в том, что 2 ^ 10 ~ = 10 ^ 3.

Другими словами, миллион примерно равен 2 ^ 20. Таким образом, вы можете просто сдвинуть сперва 20. Компилятор не сделает это автоматически, конечно, потому что он меняет результат. Но если вы готовы жить с небольшой точностью, и разделение на самом деле является реальной проблемой производительности, это будет мое предложение.

Ответ 7

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

У него даже есть страница с калькулятором Javascript для магических чисел:

Ответ 8

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

Если вы на самом деле не программируете это на ассемблере, я бы поставил против вас, что вы действительно улучшаете что-либо в процессе реализации SPARC. Возможно, если вы используете положительно древний процессор SPARC V7, то до того, как подразделение было реализовано в аппаратном обеспечении, вы можете получить некоторое улучшение, но даже тогда Я бы поставил на ускорение встроенного деления.

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

Ответ 9

Если вы можете обойти это, вот мое решение.

  • использовать целые числа вместо float (они быстрее)
  • разделите на 1048576, сдвинув биты вправо (что дешевле, чем что-либо на поплавках)

и убедите себя, что миллисекунды должны быть base2, а не base10.; -)

Ответ 10

1/1000000 - 0,000000000000000000 0100 0011 0001 1011 1101 1110 1000 0010 1101 0111 1011 0110 0011 01 двоичный - это 0x431BDE82 * 2 ^ -18

Следовательно, n/1000000 эквивалентно (n * 0x431BDE82) → 18

Также n/1000000 эквивалентно (n * 0x8637BD04) → 19

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