0. 寫在最前面
本文持續更新地址:https://haoqchen.site/2019/12/28/double-vs-float/
首先說明,如果只是一兩次的浮點運算,無腦使用double即可。下面主要針對需要大量浮點運算的情況做分析,比較float和double的優缺點。如無特殊說明,我的環境如下:
- 系統:Ubuntu1604(64bit)
- 編譯器:
g++ 5.4.0
- CPU:i7-4771
文中所有的時間計算函數使用我的另一篇博文:Linux時間相關函數總結中講到的clock_gettime
的CLOCK_THREAD_CPUTIME_ID
。
爲了避免計算時間的隨機性,每個計算運行3次。
另外吐槽個事情,最近博客閱讀量到10萬了,然後想申請CSDN的博客專家,然後CSDN給出的回覆是“都是基礎,沒有深度”:
嗯,基礎不重要。
如果覺得寫得還不錯,可以找我其他文章來看看哦~~~可以的話幫我github點個讚唄。
你的Star是作者堅持下去的最大動力哦~~~
1. 數據類型的基本情況
從《C++ Primer Plus(第6版)》瞭解到,C++定義了3種浮點類型:float、double、long double,並且要求:
- float至少32位(一般爲32位)
- double至少48位,且不少於float(一般爲64位)
- long double至少和double一樣多
關於浮點數在內存中如何存儲,這裏有一篇講得非常詳細的博客:浮點數在計算機內存中是如何存儲的?,這裏不再細述。
很明顯,float相對於double的優點有:
- 佔用內存少。這個做過單片機的同學應該深有體會,能用float的堅決不用double,用double一不小心程序就滿了。現代電腦如果不是大數據,問題都不大。
- 位數少,硬件讀取快。要從硬件讀取大量數據,或者要將大量參數保存到本地硬盤的時候需要考慮。
- 精度低,運算收斂快。要算個
cos
、sin
、log
,float只需要運算7次即可收斂,而double需要迭代8~18次。
相對的,double的優點有:
- 精度高。《C++ Primer Plus(第6版)》中舉了一個例子,10.0/3.0*1.0e6,float只有3333333.25,而double能精確到3333333.333333。。。對於精度要求非常高的運算,float的誤差是不能容忍的。
下面是我從普通加減乘除對比、硬盤讀寫對比、複雜函數三個方面做的一個實驗對比,如果有興趣也可以在你自己的電腦上做個對比。
2. 普通加減乘除對比
測試代碼:
#include <iostream>
#include <string>
#include <sstream>
#include <vector>
#include <time.h>
using std::cout;
using std::endl;
auto time2String = [](struct timespec t_start, struct timespec t_end) -> const std::string {
std::string result;
std::stringstream ss;
long double temp = 0.0;
temp += (t_end.tv_sec - t_start.tv_sec);
temp += static_cast<long double>((t_end.tv_nsec - t_start.tv_nsec) / 1000u) / static_cast<long double>(1000000.0);
ss.precision(6);
ss.setf(std::ios::fixed);
ss << temp ;
ss >> result;
return result;
};
int main(int argc, char **argv)
{
if (argc != 2){
std::cerr << "Please run as: cpp_test 1000" << std::endl \
<< "with 1000 means loop count" << std::endl;
return -1;
}
std::size_t loop_count = stoul(std::string(argv[1]));
volatile std::size_t i = 0; // 要求編譯器每次都直接讀取原始內存地址,防止編譯器對循環做優化
std::vector<float> vfloat(loop_count, 5.5f);
std::vector<double> vdouble(loop_count, 5.5);
struct timespec time_start;
if (0 != clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time_start)){
std::cerr << "get time wrong" << std::endl;
}
// float 循環做加減乘除
for (; i < loop_count; ++i){
vfloat[i] += 2.3f;
vfloat[i] -= 3.4f;
vfloat[i] *= 4.5f;
vfloat[i] /= 5.6f;
}
struct timespec time_1;
if (0 == clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time_1)){
std::cout << "float +-*/ time: " << time2String(time_start, time_1) << "s" << std::endl;
}
// double 循環做加減乘除
i = 0;
for (; i < loop_count; ++i){
vdouble[i] += 2.3;
vdouble[i] -= 3.4;
vdouble[i] *= 4.5;
vdouble[i] /= 5.6;
}
struct timespec time_2;
if (0 == clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time_2)){
std::cout << "double +-*/ time: " << time2String(time_1, time_2) << "s" << std::endl;
}
}
使用以下指令進行編譯,其中-O0
讓編譯器不做任何優化:
g++ cpp_test.cpp -std=c++11 -o cpp_test -O0
測試結果:
1百~1萬 | 10萬到1億 |
---|---|
從1百到1億,每個數量級都運算了3次以避免偶然因素。可以發現,在1百次到1萬次之間,double類型會比float耗時要多得多,而到了10萬次以上,兩者的時間就相差不大了。
實驗到這裏,表示非常的疑惑,數量級非常小的時候,差距反而擴大了?仔細檢查後發現,竟然是std::cout
惹的禍,將float後面的輸出註釋掉,再運行,double的時間就跟float相差不大了。
總結: 對於普通的加減乘除而言,如果數據量少於10萬,兩者的運算效率幾乎沒有差距,而數據量大於10萬以後,float要略微優於double7~8個百分點。
注意:
-
這裏千萬記得賦值給float時要在數字後面加上
f
,不要寫成下面這樣。因爲對於系統而言,默認的浮點型是double,如果你不帶後面的f
,只寫數字,那麼這個數字就是double類型的,要賦值給float需要經過一次類型轉換
。我將上面的代碼中的f
去掉後進行了一次測試,結果顯示,float的耗時甚至會反超double6~7個百分點。在優化等級爲O2
的情況下耗時增加更加明顯,float的耗時甚至超過了double20個百分點。for (; i < loop_count; ++i){ vfloat[i] += 2.3; vfloat[i] -= 3.4; vfloat[i] *= 4.5; vfloat[i] /= 5.6; }
-
對於普通的加減乘除,兩者的運算時間不會有太大的區別。因爲目前CPU中的加法器位數遠不止64位,運算一個32位和運算一個64位的數據對他們來說沒有太大區別。但對於一些優化指令集就不一樣了,比如SSE(僅支持單精度的浮點運算)、SSE2、SSE3等。這些指令集有專門對整形和浮點型做運算優化,尤其是一些向量運算和矩陣運算。比如SSE2使用了128位的運算單元,可以同時運算4個32位的浮點數或者2個64位的浮點數。根據g++官方文檔的說法,g++是默認開啓
SSE×
指令集的,但上面其實並沒有並行運算,所以效果並不明顯。具體可對比本文第4章。
2. 硬盤讀寫對比
測試代碼:
int main(int argc, char **argv)
{
if (argc != 2){
std::cerr << "Please run as: cpp_test 1000" << std::endl \
<< "with 1000 means loop count" << std::endl;
return -1;
}
std::size_t loop_count = stoul(std::string(argv[1]));
volatile std::size_t i = 0; // 要求編譯器每次都直接讀取原始內存地址,防止編譯器對循環做優化
float data_float = 0.1018f;
double data_double = 0.1018;
struct timespec time_1;
struct timespec time_2;
struct timespec time_3;
struct timespec time_4;
struct timespec time_start;
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time_start);
// float 循環寫入硬盤
i = 0;
std::ofstream float_out;
float_out.open("./float_out.txt", std::ios::out | std::ios::trunc); // 如果存在則先刪除
float_out.setf(std::ios::fixed);
float_out.precision(7);
for (; i < loop_count; ++i){
float_out << data_float;
}
float_out.close();
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time_1);
// float 循環讀取硬盤
i = 0;
std::ifstream float_in;
float_in.open("./float_out.txt", std::ios::in);
float_in.setf(std::ios::fixed);
float_in.precision(7);
for (; i < loop_count; ++i){
float_in >> data_float;
// std::cout << data_float << ",";
}
// std::cout << std::endl;
float_in.close();
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time_2);
// double 循環寫入硬盤
i = 0;
std::ofstream double_out;
double_out.open("./double_out.txt", std::ios::out | std::ios::trunc); // 如果存在則先刪除
double_out.setf(std::ios::fixed);
double_out.precision(14);
for (; i < loop_count; ++i){
double_out << data_double;
}
double_out.close();
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time_3);
// double 循環讀取硬盤
i = 0;
std::ifstream double_in;
double_in.open("./double_out.txt", std::ios::in);
double_in.setf(std::ios::fixed);
double_in.precision(14);
for (; i < loop_count; ++i){
double_in >> data_double;
// std::cout << data_double << ",";
}
// std::cout << std::endl;
double_in.close();
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time_4);
std::cout << "float write time: " << time2String(time_start, time_1) << "s" << std::endl;
std::cout << "double write time: " << time2String(time_2, time_3) << "s" << std::endl;
std::cout << "float read time: " << time2String(time_1, time_2) << "s" << std::endl;
std::cout << "double read time: " << time2String(time_3, time_4) << "s" << std::endl;
}
測試結果:
測試的是否發現,先讀寫float還是讀寫double對時間的影響非常大,尤其是次數比較少時,第一個讀寫的類型消耗的時間會增加很多。爲了公平起見,分別將float和double都放到了前面進行測試。如下圖所示,其中左邊的是double放在前面的測試時間,右邊的是float放在前面的測試時間。
10~1萬 | 10萬到1億 |
---|---|
總結:
- 首先對於10~1萬的數據,初步猜測哪個放前面,哪個消耗的時間就比較多的原因是,程序獲取硬盤的系統資源需要一定的時間,這個時間幾乎都消耗在第一次獲取的時間上,後面該程序會一直持有,時間不再消耗在獲取上。
- 對於10~1萬的數據,綜合兩個順序的時間來看,double仍然要比float慢一點。
- 寫入的時間大學比讀取的時間慢40%。
- 對於10萬~1億的數據,無論哪種順序,double都要比float慢,寫入的速度float:double最後差不多穩定在32:39,讀取速度差不多穩定在23:29。
- 當寫入量達到1億的時候,生成的文件大小對比:
3. 複雜函數
測試代碼:
int main(int argc, char **argv)
{
if (argc != 2){
std::cerr << "Please run as: cpp_test 1000" << std::endl \
<< "with 1000 means loop count" << std::endl;
return -1;
}
std::size_t loop_count = stoul(std::string(argv[1]));
volatile std::size_t i = 0; // 要求編譯器每次都直接讀取原始內存地址,防止編譯器對循環做優化
std::vector<float> vfloat(loop_count, 0.0f);
std::vector<double> vdouble(loop_count, 0.0);
for (i = 0; i < loop_count; ++i){
vfloat[i] = (float)loop_count + 0.1234567f;
vdouble[i] = (double)loop_count + 0.1234567;
}
struct timespec time_1;
struct timespec time_2;
struct timespec time_start;
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time_start);
// float 循環做cos
i = 0;
for (; i < loop_count; ++i){
vfloat[i] = cosf(vfloat[i]);
vfloat[i] = sinf(vfloat[i]);
}
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time_1);
// double 循環做cos
i = 0;
for (; i < loop_count; ++i){
vdouble[i] = cos(vdouble[i]);
vdouble[i] = sin(vdouble[i]);
}
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time_2);
std::cout << "float cos and sin time: " << time2String(time_start, time_1) << "s" << std::endl;
std::cout << "double cos and sin time: " << time2String(time_1, time_2) << "s" << std::endl;
}
測試結果:
10~1萬 | 10萬到1億 |
---|---|
總結:
- double比float快,而且快不止一點點
- 在10萬和1000萬這兩個點,有點迷,搞不懂,我重複測了幾次,結果也是相差不多,很奇怪,暫時沒有解釋。換了一個固定的
cos
值,在1000萬時時間比變成0.26:0.38,但10萬測試多次仍有很大隨機性。 - 在查找
cos
的頭文件發現,math.h
中的函數都是用了SIMD
來實現的,SIMD是Single Instruction,Multiple Data的縮寫——意爲單指令多數據。具體細節還沒時間研究,可以肯定的是,這裏面利用了SSE等指令集進行了並行優化。 - 另外也測試了
log
和logf
的區別,兩者的時間差會更加明顯,最終穩定的時間比約1.6:2.5
4. Eigen矩陣運算
測試代碼:
int main(int argc, char **argv)
{
if (argc != 2){
std::cerr << "Please run as: cpp_test 1000" << std::endl \
<< "with 1000 means loop count" << std::endl;
return -1;
}
std::size_t loop_count = stoul(std::string(argv[1]));
volatile std::size_t i = 0; // 要求編譯器每次都直接讀取原始內存地址,防止編譯器對循環做優化
MatrixXf float_m1 = MatrixXf::Random(loop_count, loop_count);
MatrixXf float_m2 = MatrixXf::Random(loop_count, loop_count);
MatrixXd double_m1 = MatrixXd::Random(loop_count, loop_count);
MatrixXd double_m2 = MatrixXd::Random(loop_count, loop_count);
struct timespec time_1;
struct timespec time_2;
struct timespec time_start;
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time_start);
// float 循環做加減乘除
MatrixXf float_p = float_m1 * float_m2;
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time_1);
// double eigen矩陣相乘
MatrixXd double_p = double_m1 * double_m2;
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time_2);
std::cout << "float Eigen * time O0: " << time2String(time_start, time_1) << "s" << std::endl;
std::cout << "double Eigen * time O0: " << time2String(time_1, time_2) << "s" << std::endl;
}
使用以下命令進行編譯:
g++ cpp_test.cpp -std=c++11 -o cpp_test -I /usr/include/eigen3 -O0
測試結果:
乘法 | 加法 |
---|---|
總結:
Eigen內部是利用SSE指令集進行優化的,當數據量非常大的時候,double所用的時間就接近於float的兩倍了,這是非常明顯的提升。
參考
- C++ Primer Plus(第6版)
- Eigen的速度爲什麼這麼快?
喜歡我的文章的話Star一下唄Star
版權聲明:本文爲白夜行的狼原創文章,未經允許不得以任何形式轉載