Чем C++ math.h abs() отличается от моего abs()

В настоящее время я пишу некоторые glsl-подобные векторные математические классы на С++, и я только что реализовал функцию abs(), подобную этой:

template<class T>
static inline T abs(T _a)
{
    return _a < 0 ? -_a : _a;
}

Я сравнил его скорость с дефолтным C++ abs из math.h вот так:

clock_t begin = clock();
for(int i=0; i<10000000; ++i)
{
    float a = abs(-1.25);
};

clock_t end = clock();
unsigned long time1 = (unsigned long)((float)(end-begin) / ((float)CLOCKS_PER_SEC/1000.0));

begin = clock();
for(int i=0; i<10000000; ++i)
{
    float a  = myMath::abs(-1.25);
};
end = clock();
unsigned long time2 = (unsigned long)((float)(end-begin) / ((float)CLOCKS_PER_SEC/1000.0));

std::cout<<time1<<std::endl;
std::cout<<time2<<std::endl;

Теперь пресс по умолчанию занимает около 25 мс, в то время как мой занимает 60. Я предполагаю, что происходит некоторая низкоуровневая оптимизация. Кто-нибудь знает, как math.h abs работает внутри? Разница в производительности не существенна, но мне просто любопытно!


person moka    schedule 20.05.2010    source источник
comment
Возможно, вы могли бы показать исходный код сборки, сгенерированный компилятором для двух циклов, и сравнить его. В противном случае мы просто предполагаем.   -  person Greg Hewgill    schedule 21.05.2010
comment
abs принимает целое число и возвращает целое число. Так что использование abs просто неправильно в вашем сравнении. Вместо этого используйте fabs из <cmath>.   -  person kennytm    schedule 21.05.2010
comment
И, пожалуйста, расскажите нам, какой у вас компилятор/окружение.   -  person jpalecek    schedule 21.05.2010
comment
Я использую GCC 4.2 в xCode на Intel Mac i7.   -  person moka    schedule 21.05.2010
comment
Хорошо, я убедился, что теперь использую std::abs, который должен быть перегружен. к моему удивлению, один намного медленнее моего и занял 146 мс   -  person moka    schedule 21.05.2010
comment
@moka: вот и хорошо! Ваш код лучше реализации, ура :)   -  person jpalecek    schedule 21.05.2010
comment
ах, я идиот, я был в режиме отладки, теперь, когда я в релизе, обе функции идентичны по скорости :)   -  person moka    schedule 21.05.2010


Ответы (8)


Поскольку они являются реализацией, они могут делать столько предположений, сколько захотят. Они знают формат double и могут вместо этого подшутить над ним.

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

template <>
double abs<double>(const double x)
{
    // breaks strict aliasing, but compiler writer knows this behavior for the platform
    uint64_t i = reinterpret_cast<const std::uint64_t&>(x);
    i &= 0x7FFFFFFFFFFFFFFFULL; // clear sign bit

    return reinterpret_cast<const double&>(i);
}

Это устраняет ветвление и может работать быстрее.

person GManNickG    schedule 20.05.2010
comment
Не будет ли это вопиющим примером неопределенного поведения? - person jpalecek; 21.05.2010
comment
@jpalecek: я не вижу там никакого UB. reinterpret_cast имеет поведение, определяемое реализацией. (Отсюда моя точка зрения, что они могут делать любые предположения о реализации, которые они хотят, поскольку они являются реализацией.) Кодировщики, не работающие в библиотеке, не могут. Когда вы пишете этот код, вы предполагаете определенные вещи о своей реализации. - person GManNickG; 21.05.2010
comment
Если дубль не IEEE-754, то да. Это, по крайней мере, менее портативно - person Yann Ramin; 21.05.2010
comment
Не могли бы вы просто вернуть a? - person fredoverflow; 21.05.2010
comment
Дело в том, что реализация может написать этот код, а вы нет. - person rlbond; 21.05.2010
comment
@Fred: я не понимаю, что ты имеешь в виду. - person GManNickG; 21.05.2010
comment
@GMan: я не говорю о reinterpret_cast, я имел в виду 3.10.15 (строгое нарушение псевдонимов): если программа пытается получить доступ к сохраненному значению объекта через lvalue, отличное от одного из следующих типов, поведение не определено: (здесь идет перечисление случаев). Доступ к double через lvalue типа uint64_t в перечислении не упоминается. - person jpalecek; 21.05.2010
comment
@Jpalecek: это тоже может быть неопределенно для вас. Но для разработчиков библиотеки она хорошо определена (поскольку математическая библиотека тесно связана с компилятором). Они знают, что делает реализация, и поэтому могут выполнять определенные оптимизации на основе этих знаний. С другой стороны, вы не можете делать никаких предположений, поскольку ваш код не связан с компилятором. - person Martin York; 21.05.2010
comment
@jpalecek: Как указывает @Martin, есть причина, по которой весь мой ответ начинается с того, что поскольку они являются реализацией, они могут делать столько предположений, сколько захотят. - person GManNickG; 21.05.2010
comment
@Martin York: Кажется, вы упускаете суть - это поведение не определяется реализацией, оно не определено, и компиляторы реального мира фактически рассматривают его как таковое. Это означает, что они могут и регулярно компилируют такой код в no-op, в зависимости от уровня оптимизации и множества других вещей. Даже напр. когда он дважды встроен в одну и ту же программу, он может работать в одном случае, а не в другом. И автор библиотеки ничего не может с этим поделать, кроме правильного написания кода (что может быть ((unsigned char*)&d)[7] &= 0x7f на машинах Intel). - person jpalecek; 21.05.2010
comment
@GMan: Возможно, они могут делать предположения о поведении компилятора выше стандарта C (что может быть или не быть мудрым), но вы не сделали достаточно предположений для того, чтобы ваш код работал. - person jpalecek; 21.05.2010
comment
@jpalecek: я отредактировал свой ответ, теперь он понятнее? Поставщик компилятора может предполагать все, что захочет, потому что он компилятор. Когда вы компилируете для конкретной платформы, вы знаете, какие допущения вы можете делать, а какие нет. - person GManNickG; 21.05.2010
comment
@GMan: Нет. Этот вопрос помечен как c++, и если вы хотите сделать какие-либо предположения, выходящие за рамки стандарта, вы должны написать их конкретно. С вашим компилятором автор может принять любую доктрину, вы также можете сказать, что d/0 возвращает абсолютное значение d. Я надеюсь, вы согласны, что это будет просто чушь, а не полезное решение. - person jpalecek; 21.05.2010
comment
@jpalecek: Я не понимаю, почему вы не видите сути вопроса и ответа. Вопрос в том, почему эта реализация быстрее? Я отвечаю, потому что они могут делать какие угодно предположения, вот пример. Это не становится намного проще, чем это. Конечно, он помечен как C++, потому что вопрос касается C++. Но это также касается реализации компилятора C++. И да, если d/0 возвращает абсолютное значение на какой-то платформе, производитель компилятора может и должен этим воспользоваться. Я действительно не понимаю, как сложно понять, что разработчики могут предполагать все, что хотят. - person GManNickG; 21.05.2010
comment
@jpalecek: На самом деле; Я думаю, что это вы, кажется, упускаете суть. - person Martin York; 21.05.2010
comment
@moka У меня была похожий вопрос на днях. Чтобы процитировать IEEE 754 - 2008: Comparisons shall ignore the sign of zero (so +0 = −0). Оказывается, ваше предложение и код ОП не эквивалентны. Для -0.0 ваша версия возвращает 0.0, а OT возвращает -0.0. Я не знаю, какая реализация правильная, но из того, что я видел в других библиотеках, я бы предположил, что ОП недействителен. - person FirefoxMetzger; 30.08.2016

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

int my_abs(int x)
{
    int s = x >> 31;
    return (x ^ s) - s;
}
person fredoverflow    schedule 20.05.2010
comment
Значения с плавающей запятой не хранятся в дополнении до двух, но это хитрый прием для абсолютного значения. (Для определения знака я делаю тот же первый шаг, но затем возвращаю s | 1; -1 = отрицательный, +1 = положительный) - person dash-tom-bang; 21.05.2010
comment
@jpal Достаточно справедливо, но где именно мока заявляет, что его интересуют только типы с плавающей запятой? Может быть, тогда template<class T> следует заменить на template<class Floating>, просто для ясности? - person fredoverflow; 21.05.2010

Какой у вас компилятор и настройки? Я уверен, что MS и GCC реализуют «внутренние функции» для многих математических и строковых операций.

Следующая строка:

printf("%.3f", abs(1.25));

попадает в следующий путь кода "fabs" (в msvcr90d.dll):

004113DE  sub         esp,8 
004113E1  fld         qword ptr [__real@3ff4000000000000 (415748h)] 
004113E7  fstp        qword ptr [esp] 
004113EA  call        abs (4110FFh) 

abs называют реализацию среды выполнения C "fabs" на MSVCR90D (довольно большой):

102F5730  mov         edi,edi 
102F5732  push        ebp  
102F5733  mov         ebp,esp 
102F5735  sub         esp,14h 
102F5738  fldz             
102F573A  fstp        qword ptr [result] 
102F573D  push        0FFFFh 
102F5742  push        133Fh 
102F5747  call        _ctrlfp (102F6140h) 
102F574C  add         esp,8 
102F574F  mov         dword ptr [savedcw],eax 
102F5752  movzx       eax,word ptr [ebp+0Eh] 
102F5756  and         eax,7FF0h 
102F575B  cmp         eax,7FF0h 
102F5760  jne         fabs+0D2h (102F5802h) 
102F5766  sub         esp,8 
102F5769  fld         qword ptr [x] 
102F576C  fstp        qword ptr [esp] 
102F576F  call        _sptype (102F9710h) 
102F5774  add         esp,8 
102F5777  mov         dword ptr [ebp-14h],eax 
102F577A  cmp         dword ptr [ebp-14h],1 
102F577E  je          fabs+5Eh (102F578Eh) 
102F5780  cmp         dword ptr [ebp-14h],2 
102F5784  je          fabs+77h (102F57A7h) 
102F5786  cmp         dword ptr [ebp-14h],3 
102F578A  je          fabs+8Fh (102F57BFh) 
102F578C  jmp         fabs+0A8h (102F57D8h) 
102F578E  push        0FFFFh 
102F5793  mov         ecx,dword ptr [savedcw] 
102F5796  push        ecx  
102F5797  call        _ctrlfp (102F6140h) 
102F579C  add         esp,8 
102F579F  fld         qword ptr [x] 
102F57A2  jmp         fabs+0F8h (102F5828h) 
102F57A7  push        0FFFFh 
102F57AC  mov         edx,dword ptr [savedcw] 
102F57AF  push        edx  
102F57B0  call        _ctrlfp (102F6140h) 
102F57B5  add         esp,8 
102F57B8  fld         qword ptr [x] 
102F57BB  fchs             
102F57BD  jmp         fabs+0F8h (102F5828h) 
102F57BF  mov         eax,dword ptr [savedcw] 
102F57C2  push        eax  
102F57C3  sub         esp,8 
102F57C6  fld         qword ptr [x] 
102F57C9  fstp        qword ptr [esp] 
102F57CC  push        15h  
102F57CE  call        _handle_qnan1 (102F98C0h) 
102F57D3  add         esp,10h 
102F57D6  jmp         fabs+0F8h (102F5828h) 
102F57D8  mov         ecx,dword ptr [savedcw] 
102F57DB  push        ecx  
102F57DC  fld         qword ptr [x] 
102F57DF  fadd        qword ptr [__real@3ff0000000000000 (1022CF68h)] 
102F57E5  sub         esp,8 
102F57E8  fstp        qword ptr [esp] 
102F57EB  sub         esp,8 
102F57EE  fld         qword ptr [x] 
102F57F1  fstp        qword ptr [esp] 
102F57F4  push        15h  
102F57F6  push        8    
102F57F8  call        _except1 (102F99B0h) 
102F57FD  add         esp,1Ch 
102F5800  jmp         fabs+0F8h (102F5828h) 
102F5802  mov         edx,dword ptr [ebp+0Ch] 
102F5805  and         edx,7FFFFFFFh 
102F580B  mov         dword ptr [ebp-0Ch],edx 
102F580E  mov         eax,dword ptr [x] 
102F5811  mov         dword ptr [result],eax 
102F5814  push        0FFFFh 
102F5819  mov         ecx,dword ptr [savedcw] 
102F581C  push        ecx  
102F581D  call        _ctrlfp (102F6140h) 
102F5822  add         esp,8 
102F5825  fld         qword ptr [result] 
102F5828  mov         esp,ebp 
102F582A  pop         ebp  
102F582B  ret   

В режиме выпуска вместо этого используется инструкция FPU FABS (занимает 1 такт только на FPU >= Pentium), вывод дизассемблирования:

00401006  fld         qword ptr [__real@3ff4000000000000 (402100h)] 
0040100C  sub         esp,8 
0040100F  fabs             
00401011  fstp        qword ptr [esp] 
00401014  push        offset string "%.3f" (4020F4h) 
00401019  call        dword ptr [__imp__printf (4020A0h)] 
person Hernán    schedule 20.05.2010

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

person sepp2k    schedule 20.05.2010
comment
эй, я не очень разбираюсь в побитовом программировании, как это будет работать? - person moka; 21.05.2010

Может быть несколько вещей:

  • вы уверены, что первый вызов использует std::abs? С таким же успехом можно было бы использовать целое число abs из C (либо вызвать std::abs явно, либо иметь using std::abs;)

  • компилятор может иметь встроенную реализацию некоторых функций с плавающей запятой (например, компилировать их непосредственно в инструкции FPU)

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

person jpalecek    schedule 20.05.2010

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

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

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

person dash-tom-bang    schedule 20.05.2010

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

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

person Matteo Italia    schedule 20.05.2010

Библиотечная функция abs работает с целыми числами, в то время как вы, очевидно, тестируете числа с плавающей запятой. Это означает, что вызов abs с аргументом float включает в себя преобразование из float в int (может быть без операции, поскольку вы используете константу, и компилятор может сделать это во время компиляции), затем операцию INTEGER abs и преобразование int->float. Ваша шаблонная функция будет включать операции с поплавками, и это, вероятно, имеет значение.

person Tomek    schedule 21.05.2010
comment
std::abs существует также для поплавков. См. en.cppreference.com/w/cpp/numeric/math/fabs - person vll; 10.01.2020