在做一個活動的需求時,需要往redis中有序的集合中存儲一個小數,結果發現取出數據和存儲時的數據不一致
zadd test_2017 1.1 tom (integer) 1 zrevrange test_2017 0 -1 withscores 1) "tom" 2) "1.1000000000000001" zadd test_2017 1.2 sam (integer) 1 zrevrange test_2017 0 -1 withscores 1) "sam" 2) "1.2" 3) "tom" 4) "1.1000000000000001"
是不是很奇怪, 存儲tom的score 為1.1,結果為 1.1000000000000001,存儲 sam的score為1.2,結果就是1.2
1.19999999999999995559 第16位是9,第17位是5, 四舍五入1.2
1.10000000000000008882 第16位是0, 第17位是8,四舍五入1.10000000000000001
但在php中
<?php echo 1.1;
確實顯示1.1,是不是有點奇怪,后來在php.ini中找到到precision這個選項, 指
; The number of significant digits displayed in floating point numbers. ; http://php.net/precision precision = 14
因為在php中,小數都是以double形式存儲的,那么1.10000000000000008882中第14位為0,第15位也為0,四舍五入,為1.1
解決方法:php在處理時使用bcmul函數,將小數*100,再存入redis,待取出來時,再除以100
看了下redis zadd的源碼,發現 zadd key score name 這個命令,redis利用strtod這個函數 將score 轉為double浮點數
/* This generic command implements both ZADD and ZINCRBY. */ void zaddGenericCommand(redisClient *c, int incr) { static char *nanerr = "resulting score is not a number (NaN)"; robj *key = c->argv[1]; robj *ele; robj *zobj; robj *curobj; double score = 0, *scores, curscore = 0.0; int j, elements = (c->argc-2)/2; int added = 0; if (c->argc % 2) { addReply(c,shared.syntaxerr); return; } /* Start parsing all the scores, we need to emit any syntax error * before executing additions to the sorted set, as the command should * either execute fully or nothing at all. */ scores = zmalloc(sizeof(double)*elements); for (j = 0; j < elements; j++) { if (getDoubleFromObjectOrReply(c,c->argv[2+j*2],&scores[j],NULL) != REDIS_OK) { zfree(scores); return; } } 。。。。 }
int getDoubleFromObjectOrReply(redisClient *c, robj *o, double *target, const char *msg) { double value; if (getDoubleFromObject(o, &value) != REDIS_OK) { if (msg != NULL) { addReplyError(c,(char*)msg); } else { addReplyError(c,"value is not a valid float"); } return REDIS_ERR; } *target = value; return REDIS_OK; } int getDoubleFromObject(robj *o, double *target) { double value; char *eptr; if (o == NULL) { value = 0; } else { redisAssertWithInfo(NULL,o,o->type == REDIS_STRING); if (o->encoding == REDIS_ENCODING_RAW) { errno = 0; value = strtod(o->ptr, &eptr); if (isspace(((char*)o->ptr)[0]) || eptr[0] != '\0' || errno == ERANGE || isnan(value)) return REDIS_ERR; } else if (o->encoding == REDIS_ENCODING_INT) { value = (long)o->ptr; } else { redisPanic("Unknown string encoding"); } } *target = value; return REDIS_OK; }
利用strtod寫個小程序
#include <stdio.h> #include <stdlib.h> int main() { char str[30] = "1.1 This is test"; char *ptr; double ret; ret = strtod(str, &ptr); printf("%.51f",ret); return(0); }
結果是 1.100000000000000088817841970012523233890533447265625
double雙精度浮點數據的有效位是16位(針對10進制來說),
也就是 printf("%.16f", ret) 那面上面的數據就是 1.1000000000000001, 也就是根據第17位是8,四舍五入
看了下網上關於strtod源碼,感覺返回的就是1.1,但賦值給double類型的 ret,才有了上面的值
浮點數和基本類型數據的存儲差別比較大,這里不是說存儲形式的差別,而是浮點數存放的時候是要經過運算后再轉換成整數的4字節或8字節的形式,然后再存放到內存里。因此,只通過16進制數是看不出來和整數有什么差別
在內存中保存小數,使用的是科學計數法
比如 123.456 用十進制科學計數法可以表達為 1.23456 × 102 ,其中 1.23456 為尾數,10 為基數,2 為指數
在 IEEE 標准中,浮點數是將特定長度的連續字節的所有二進制位分割為特定寬度的符號域,指數域和尾數域三個域,其中保存的值分別用於表示給定二進制浮點數中的符號,指數和尾數。這樣,通過尾數和可以調節的指數(所以稱為"浮點")就可以表達給定的數值了。具體的格式:
符號位 階碼 尾數 長度
float 1 8 23 32
double 1 11 52 64
http://blog.csdn.net/jjj19891128/article/details/22945441
http://www.cnblogs.com/dolphin0520/archive/2011/10/02/2198280.html
那么,我們先來看32位浮點數 的換算:
1. 從浮點數到16進制數
float var = 5.2f;
就這個浮點數,我們一步一步將它轉換為16進制數。
首先,整數部分5,4位二進制表示為:0101。
其次,小數部分0.2,我們應該學了小數轉換為二進制的計算方法,那么就是依次乘以2,取整數部分作為二進制數,取小數部分繼續乘以2,一直算到小數結果為0為止。那么對0.2進行計算:
0.2*2 = 0.4 * 2 = 0.8 * 2 = 1.6(0.6) * 2 = 1.2(0.2)*2 = 0.4 * 2 = 0.8 * 2 = 1.6(0.6) * 2 = 1.2 ... ...
0 0 1 1 0 0 1 1 ... ...
因此,這里把0.2的二進制就計算出來了,結果就為:0.00110011... ... 這里的省略號是你沒有辦法計算完。二進制序列無限循環,沒有到達結果為0的那一天。那么此時我們該怎么辦?這里就得取到一定的二進制位數后停止計算,然后舍入。我們知道,float是32位,后面尾數的長度只能最大23位。因此,計算結束的時候,整數部分加上小數部分的二進制一共23位二進制。因此5.2的二進制表示就為:
101.00110011001100110011
一共23位。
此時,使用科學計數法表示,結果為:
1.0100110011001100110011 * 22
由於我們規定,使用二進制科學計數法后,小數點左邊必須為1(肯定為1嘛,為0的話那不就是0.xxxx*sxxx 了,這樣沒有什么意義),這里不能為0是有一個很大的好處的,為什么?因為規定為1,這樣這個1就不用存儲了,我們在從16進制數換算到浮點數的時候加上這個1就是了,因為我們知道這里應該有個1,省略到這個1的目的是為了后面的小數部分能夠多表示一位,精度就更高一些了喲。那么省略到小數點前面的1后的結果為:
.01001100110011001100110 * 22
這里后面藍色的0就是補上的,這里不是隨便補的一個0,而是0.2的二進制在這一位上本來就應該為0,如果該為1,我們就得補上一個1.是不是這樣多了一位后,實際上我們用23位表示了24位的數據量。有一個位是隱藏了,固定為1的。我們不必記錄它。
但是,在對階或向右規格化時,尾數要向右移位,這樣被右移的尾數的低位部分會被丟掉,從而造成一定的誤差,因此要進行舍入處理。 常用的舍入方法有兩種:一種是“0舍1入”法,即如果右移時被丟掉數位的最高位為0則舍去,為1則將尾數的末位加“1”,另一種是“恆置1”,即只要數位被移掉,就在尾數的末位恆置“1”。
舉個例子:
123.456的二進制表示:
123.456的二進制到23位時:111 1011.0111 0100 1011 1100 01...
后面還有依次為01...等低位,由於最高位的1會被隱藏,向后擴展一位如果不做舍入操作則結果為:
1.11 1011 0111 0100 1011 1100 0 * 26
但是經過舍入操作后,由於被舍掉的位的最高位是1,或者“恆置1”法,最后面的0都應該是1。因此最終就應該是:
1.11 1011 0111 0100 1011 1100 1 * 26
在這里需要說明,不管是恆置1,還是0舍1入法,其根本都是為了減小誤差。
好了,尾數在這里就計算好了,他就是 01001100110011001100110 。
再來看階數,這里我們知道是2^2次方,那么指數就是2。同樣IEEE標准又規定了,因為中間的 階碼在float中是占8位,而這個 階碼又是有符號的(意思就是說,可以有2^-2次方的形式)。
float 類型的 偏置量 Bias = 2k-1 -1 = 28-1 -1 = 127 ,但還要補上剛才因為左移作為小數部分的 2 位(也就是科學技術法的指數),因此偏置量為 127 + 2=129 ,就是 IEEE 浮點數表示標准:
V = (-1)s × M × 2E
E = e - Bias
中的 e ,此前計算 Bias=127 ,剛好驗證了 E = 129 - 127 = 2 。
這里的階碼就是12910 ,二進制就是:1000 00012 。
因此,拼接起來后:
1000 0001 01001100110011001100110
| ← 8位 → | | ←------------- 23位 -------------→ |
一共就是31位了,這里還差一位,那就是符號位,我們定義的是5.2,正數。因此這里最高位是0,1表示負數。
而后結果就是:
0 1000 0001 01001100110011001100110
1位 | ← 8位 → | | ←-------------- 23位 ------------→ |
到這里,我們內存里面的十六進制數產生了,分開來看:
0 100 0000 1 010 0110 0110 0110 0110 0110
4 0 A 6 6 6 6 6
因此,我們看到的就是0x40A66666, 此就是5.2最終的整數形式。
網上有個例子:這個例子是計算小數點后60位的,因為1.25中的整數1的二進制就是1, 進位位為0,階碼為11位,還剩 64-1-11=52位,所以有效位為52位
<?php $bin = ""; $int = 15; $base = 100; for ($i = 0; $i <= 60; $i++) { $int = $int * 2; if ($int == $base) { $bin.="1"; break; } if ($int >$base) { $bin.="1"; $int = $int - $base; } else { $bin .= "0"; } } echo $bin; echo "\n"; echo "現在的長度是".strlen($bin); echo "\n"; echo"\n"; echo "52位長度的二進制\n"; $bin=substr($bin,0,52); echo $bin."\n"; $f = 1; $l = strlen($bin); for ($i = 0; $i < $l; $i++) { if ($bin[$i] > 0) { $f = $f + pow(2, -($i + 1)); } } echo "反計算后數值\n"; echo number_format($f, 30); echo "\n"; echo "1.15本身的30位數據\n"; $f = 1.15; echo number_format($f, 30); echo "\n";
結果
0010011001100110011001100110011001100110011001100110011001100
現在的長度是61
52位長度的二進制
0010011001100110011001100110011001100110011001100110
反計算后數值
1.149999999999999911182158029987
1.15本身的30位數據
1.149999999999999911182158029987
經比較發現,61位的二進制和52的二進制 反向計算后的結果是一樣的
