#java #c_sharp #cpp #c
Существует множество противоречивой информации по поводу производительности float и double на платформе x86-64. Хотелось бы разобраться в этом вопросе. Так как на этот вопрос сложно дать однозначный ответ и как правило используют вещественные с двойной точностью, предлагаю рассмотреть ситуации, в которых действительно стоит использовать именно float вместо double
Ответы
Ответ 1
TL;DR: float, как и ожидалось, быстрее double, поэтому, если вы работаете с большими объемами данных и вам хватает точности float, то вы выбираете float. Если точности float недостаточно, то ваш выбор невелик — double. Если у вас вообще нет никаких притязаний — выбирайте что угодно, разницы не увидите. Я, как участник вышеозначенного спора, решил написать ответ. Для того, чтобы понять, какая будет производительность, я решил сначала изучить немного теории, для этого я написал следующий код: #includeint main() { volatile double darray[] = {5.234234, 2.2143213, 3.214212, 4.123155}; volatile float farray[] = {5.234234f, 2.2143213f, 3.214212f, 4.123155f}; volatile double dres = 0.0; volatile float fres = 0.0f; for(size_t i = 0; i < 4; ++i) dres += darray[i]; for(size_t i = 0; i < 4; ++i) fres += farray[i]; fres = 0.0; } Для которого мы имеем следующий ассемблер(gcc): mov rax, QWORD PTR [rbp-96] movsd xmm0, QWORD PTR [rbp-64+rax*8] movsd xmm1, QWORD PTR [rbp-104] addsd xmm1, xmm0 movq rax, xmm1 mov QWORD PTR [rbp-104], rax add QWORD PTR [rbp-96], 1 .L2: cmp QWORD PTR [rbp-96], 3 jbe .L3 mov QWORD PTR [rbp-88], 0 jmp .L4 .L5: mov rax, QWORD PTR [rbp-88] movss xmm0, DWORD PTR [rbp-80+rax*4] movss xmm1, DWORD PTR [rbp-108] addss xmm1, xmm0 movd eax, xmm1 mov DWORD PTR [rbp-108], eax add QWORD PTR [rbp-88], 1 .L4: cmp QWORD PTR [rbp-88], 3 jbe .L5 Это не весь вывод, но тут достаточно информации. Для нас интересны тут две инструкции: addss, addsd — каждая является SIMD инструкция по работе с float(первая) и double. Первая мысль — надо поискать мануал, может там написано, что быстрее? Такой мануал есть, но беглый осмотр показал, что ответа я там не получу — судя по мануалу эти инструкции должны исполняться одинаково быстро. Хорошо. Оставим этот путь и попробуем собрать предыдущий код с AVX2 в студии, получим следующим asm: ; 6 : ; 7 : volatile double dres = 0.0; ; 8 : volatile float fres = 0.0f; ; 9 : for(size_t i = 0; i < 4; ++i) xor eax, eax vxorps xmm2, xmm2, xmm2 vmovsd QWORD PTR dres$[rsp], xmm0 vmovss DWORD PTR fres$[rsp], xmm2 mov ecx, eax npad 9 $LL4@main: ; 10 : dres += darray[i]; vmovsd xmm1, QWORD PTR darray$[rsp+rcx*8] vmovsd xmm0, QWORD PTR dres$[rsp] inc rcx vaddsd xmm1, xmm1, xmm0 vmovsd QWORD PTR dres$[rsp], xmm1 cmp rcx, 4 jb SHORT $LL4@main npad 1 $LL7@main: ; 11 : for(size_t i = 0; i < 4; ++i) ; 12 : fres += farray[i]; vmovss xmm1, DWORD PTR farray$[rsp+rax*4] vmovss xmm0, DWORD PTR fres$[rsp] inc rax vaddss xmm1, xmm1, xmm0 vmovss DWORD PTR fres$[rsp], xmm1 cmp rax, 4 jb SHORT $LL7@main Код практически не изменился, кроме того, что операции стали называться vaddsd и vaddss. Я не стал лезть в мануал по этим командам, полагаю что ситуация там схожа с теми, что мы видели ранее. Тогда пойдём другим путём: мы знаем, что float является 32-х битным, тогда как double является 64-х битным. Это неминуемо должно сказаться на производительности, вопрос только один — как? Моё знание SIMD инструкций весьма ограничено, поэтому я не понимаю, почему ни gcc, ни студия не использовали какие-нибудь пакетные инструкции для сложения чисел. Кто нибудь может подсказать почему? Я уж было решил, что таких нет. Но вот эта статья утверждает, что такие есть: VADDPD и VADDPS, обе принимают аргументы размеров в 256-бит, т.е. за раз такая операция может сложить 8 float'ов или 4 double'а. Это уже что-то — float по праву меньшего размера должен быть быстрее и мы нашли, что это на самом деле так. Другим важным фактором, который может вывести float вперёд является его меньшее влияние на кэш: т.к. он в два раза меньше, то и нагрузка на кэш будет меньше. Таким образом, не распинаясь и не расписывая больше, получаем следующий вывод, который, в целом, сразу приходит в голову: float быстрее чем double. Осталось проверить это на практике, для этого используем следующий код: #include #include #include #include #include #include int main() { const size_t size = 1'000'000'000; std::vector dvector(size, 2.2143213); std::vector fvector(size, 2.2143213f); auto start = std::chrono::high_resolution_clock::now(); volatile double dres = std::accumulate(dvector.begin(), dvector.end(), 0.0); auto doubleElapsed = (std::chrono::high_resolution_clock::now() - start).count(); start = std::chrono::high_resolution_clock::now(); volatile float fres = std::accumulate(fvector.begin(), fvector.end(), 0.0f); auto floatElapsed = (std::chrono::high_resolution_clock::now() - start).count(); std::cout << "float elapsed: " << floatElapsed << "\n"; std::cout << "double elapsed: " << doubleElapsed << "\n"; float ratio = std::max (floatElapsed, doubleElapsed) / std::min (floatElapsed, doubleElapsed); std::string relation = floatElapsed < doubleElapsed ? std::string("faster") : std::string("slower"); std::cout << "float is " << ratio << " " << relation << "!\n"; } На моём PC(Haswell) этот код, собранный в 2015 студии с AVX2, даёт стабильное преимущество float в 1.2-1.3 раза, бывают пиковые значения куда выше, но я не придавал им внимания. Даже без AVX2(я пробовал разные варианты) всё выглядит точно так же. Разумеется, измерения довольно просты, а аргументация довольно поверхностная(я не ставил целью полноценное исследования, в настоящий момент у меня нет на него времени), но даже это показывает, что люди утверждающие, что нужно по умолчанию выбирать double и что double быстрее float — не правы. И ещё один тест, где я использовал интринсики для подсчёта суммы(я может не лучшим образом их использовал, но уж как есть — по другому не умею): #include #include #include #include #include #include #include float accumulate(const std::vector & vec) { __m256 res = _mm256_undefined_ps(); for(size_t i = 0; i < vec.size(); i += 8) { __m256 m1 = _mm256_load_ps(&vec[i]); res = _mm256_add_ps(m1, res); } float out[8]; _mm256_store_ps(out, res); return std::accumulate(std::begin(out), std::end(out), 0.0f); } double accumulate(const std::vector & vec) { __m256d res = _mm256_undefined_pd(); for(size_t i = 0; i < vec.size(); i += 4) { __m256d m1 = _mm256_load_pd(&vec[i]); res = _mm256_add_pd(m1, res); } double out[4]; _mm256_store_pd(out, res); return std::accumulate(std::begin(out), std::end(out), 0.0); } int main() { const size_t size = 1'000'000; std::vector dvector(size, 2.2143213); std::vector fvector(size, 2.2143213f); auto start = std::chrono::high_resolution_clock::now(); volatile double dres = accumulate(dvector); auto doubleElapsed = (std::chrono::high_resolution_clock::now() - start).count(); start = std::chrono::high_resolution_clock::now(); volatile float fres = accumulate(fvector); auto floatElapsed = (std::chrono::high_resolution_clock::now() - start).count(); std::cout << "float elapsed: " << floatElapsed << "\n"; std::cout << "double elapsed: " << doubleElapsed << "\n"; float ratio = std::max (floatElapsed, doubleElapsed) / std::min (floatElapsed, doubleElapsed); std::string relation = floatElapsed < doubleElapsed ? std::string("faster") : std::string("slower"); std::cout << "float is " << ratio << " " << relation << "!\n"; } С таким кодом, на той же машине, я получаю прирост в 2.3-2.5 раза. Ответ 2
А что такое производительность в данном контексте? Вообще-то SIMD нам явно говорит о том, что в любой вектор вместится больше float'ов, поэтому любые векторные операции над float будут всегда быстрее аналогичных - над double, если опираться только на количественные характеристики алгоритмов. Сравнивать тут нечего в таком контексте, например. Если все же хотите сравнить, то можете взять код из этого ответа, изменив тип операндов на double/float соответственно (и команды с _mm_cmpgt_epi32 на _mm_cmpgt_pd/_mm_cmpgt_ps). Замеры производительности там все есть.Ответ 3
Так как вес float и double одинаков, скорость компиляции и работы будет только зависеть от количества чисел до и после запятой, моё мнение, я использую тип float
Комментариев нет:
Отправить комментарий