最近遇到一個PHP大整數的問題,問題代碼是這樣的php
$shopId = 17978812896666957068; var_dump($shopId);
上面的代碼輸出,會把$shopId轉換成float類型,且使用了科學計數法來表示,輸出以下:html
float(1.7978812896667E+19)git
但在程序裏須要的是完整的數字做爲查找數據的參數,因此須要用的是完整的數字,當時覺得只是由於數據被轉換成科學計數法了,因而想到的解決方案是強制讓它不使用科學計數法表示:github
$shopId= number_format(17978812896666957068); var_dump($shopId);
這時候奇怪的事情出現了,輸出的是:api
17978812896666957824數組
當時沒有仔細看,對比了前十位就沒有繼續往下看,因此認爲問題解決了,等到真正根據ID去找數據的時候才發現數據查不出來,這時候才發現是數據轉換錯誤了。php7
這裏使用number_format失敗的緣由在後面會講到,當時就想到將原來的數據轉成字符串的,可是使用瞭如下方法仍然不行數據結構
$shopId= strval(17978812896666957068); var_dump($shopId); $shopId = 17978812896666957068 . ‘’; var_dump($shopId);
輸出的結果都是框架
float(1.7978812896667E+19)函數
最後只有下面這種方案是可行的:
$shopId = ‘17978812896666957068’; var_dump($shopId); // 輸出 //string(20) "17978812896666957068"
衆所周知,PHP是一門解釋型語言,因此當時就大膽地猜想PHP是在編譯期間就將數字的字面量常量轉換成float類型,並用科學計數法表示。但僅僅猜想不能知足本身的好奇心,想要看到真正實現代碼才願意相信。因而就逐步分析、探索,直到找到背後的實現。
剛開始根據這個問題直接上網搜「PHP大整數解析過程」,並無搜到答案,所以只能本身去追查。一開始對PHP的執行過程不熟悉,出發點就只能是一步一步地調試,而後
示例代碼:
// test.php $var = 17978812896666957068; var_dump($var);
一、查看opcode
經過vld查看PHP執行代碼的opcode,能夠看到,賦值的是一個ASSIGN的opcode操做
接下來就想看看ASSIGN是在哪裏執行的。
二、gdb調試
2-一、用list查看有什麼地方能夠進行斷點
2-二、暫時沒有頭緒,在1186斷點試試
結果程序走到sapi/cli/php_cli.c文件的1200行了,按n不斷下一步執行,一直到這裏就走到了程序輸出結果了:
2-四、因而能夠猜想,ASSIGN操做是在do_cli函數裏面進行的,所以對do_cli函數作斷點:break do_cli。
輸入n,不斷回車,在sapi/cli/php_cli.c文件的993行以後就走到程序輸出結果了:
2-五、再對php_execute_script函數作斷點:break php_execute_script
不斷逐步執行,發如今main/main.c文件的2537行就走到程序輸出結果了:
2-六、繼續斷點的步驟:break zend_execute_scripts
重複以前的步驟,發如今zend/Zend.c文件的1476行走到了程序輸出結果的步驟:
看到這裏的時候,第1475行裏有一個op_array,就猜想會不會是在op_array的時候就已經有值了,因而開始打印op_array的值:
打印以後並無看到有用的信息,可是其實這裏包含有很大的信息量,好比opcode的handler: ZEND_ASSIGN_SPEC_CV_RETVAL_CV_CONST_RETVAL_UNUSED_HANDLER,可是當時沒注意到,所以就想着看看op_array是怎麼被賦值的,相關步驟作了什麼。
2-七、從新從2-5的斷點開始,讓程序逐步執行,看到op_array的賦值以下:
將zend_compile_file函數運行的結果賦值給op_array了,因而break zend_compile_file,被告知zend_compile_file未定義,經過源碼工具追蹤到zend_compile_file指向的是compile_file,因而break zend_compile
發現是在Zend/zend_language_scanner.l 文件斷點了,逐步執行,看到這行pass_two(op_array),猜想可能會在這裏就有值,因此打印看看:
結果發現仍是跟以前的同樣,可是此時看到有一個opcodes的值,打印看看
看到opcode = 38,網上查到38表明賦值
2-八、因而能夠知道,在這一步以前就已獲得了ASSIGN的opcode,所以,不斷的往前找,從op_array開始初始化時就開始,逐步打印op_array->opcodes的值,一直都是null:
直到執行了CG(zend_lineno) = last_lineno;
才獲得opcode = 38 的值:
由於這一句:CG(zend_lineno) = last_lineno;是一個宏,因此也沒頭緒,接近放棄狀態。。。
因而先去了解opcode的數據結構,在深刻理解PHP內核書裏找到opcode處理函數查找這一章,給了我一些繼續下去的思路。
引用裏面的內容:
在PHP內部有一個函數用來快速的返回特定opcode對應的opcode處理函數指針:zend_vm_get_opcode_handler()函數:
知道其實opcode處理函數的命名是有如下規律的
ZEND_[opcode]_SPEC_(變量類型1)_(變量類型2)_HANDLER
根據以前調試打印出來的內容,在2-6的時候就看到了一個handler的值:
是
ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER,
找出函數的定義以下:
能夠看到,opcode操做的時候,值是從EX_CONSTANT獲取的,根據定義展開這個宏,那就是
opline->op2->execute_data->literals
這裏能夠獲得兩個信息:
一、參數的轉換在opcode執行前就作好了
二、賦值過程取值時是在op2->execute_data->literals,若是猜測沒錯的話,op2->execute_data->literals此時保存的就是格式轉換後的值,能夠打印出來驗證一下
打印結果以下:
猜測驗證正確,可是沒有看到真正作轉換的地方,仍是不死心,繼續找PHP的Zend底層作編譯的邏輯代碼。
參考開源的GitHub項目,PHP編譯階段以下圖:
猜想最有可能的是在zendparse、zend_compile_top_stmt這兩個階段完成轉換,由於這個兩個階段作的事情就是將PHP代碼轉換成opcode數組。
上網搜索了PHP語法分析相關的文章,有一篇裏面講到了解析整數的過程,所以找到了PHP真正將大整數作轉換的地方:
<ST_IN_SCRIPTING>{LNUM} { char *end; if (yyleng < MAX_LENGTH_OF_LONG - 1) { /* Won't overflow */ errno = 0; ZVAL_LONG(zendlval, ZEND_STRTOL(yytext, &end, 0)); /* This isn't an assert, we need to ensure 019 isn't valid octal * Because the lexing itself doesn't do that for us */ if (end != yytext + yyleng) { zend_throw_exception(zend_ce_parse_error, "Invalid numeric literal", 0); ZVAL_UNDEF(zendlval); RETURN_TOKEN(T_LNUMBER); } } else { errno = 0; ZVAL_LONG(zendlval, ZEND_STRTOL(yytext, &end, 0)); if (errno == ERANGE) { /* Overflow */ errno = 0; if (yytext[0] == '0') { /* octal overflow */ ZVAL_DOUBLE(zendlval, zend_oct_strtod(yytext, (const char **)&end)); } else { ZVAL_DOUBLE(zendlval, zend_strtod(yytext, (const char **)&end)); } /* Also not an assert for the same reason */ if (end != yytext + yyleng) { zend_throw_exception(zend_ce_parse_error, "Invalid numeric literal", 0); ZVAL_UNDEF(zendlval); RETURN_TOKEN(T_DNUMBER); } RETURN_TOKEN(T_DNUMBER); } /* Also not an assert for the same reason */ if (end != yytext + yyleng) { zend_throw_exception(zend_ce_parse_error, "Invalid numeric literal", 0); ZVAL_UNDEF(zendlval); RETURN_TOKEN(T_DNUMBER); } } ZEND_ASSERT(!errno); RETURN_TOKEN(T_LNUMBER); }
能夠看到,zend引擎在對PHP代碼在對純數字的表達式作詞法分析的時候,先判斷數字是否有可能會溢出,若是有可能溢出,先嚐試將其用LONG類型保存,若是溢出,先用zend_strtod將其轉換爲double類型,而後用double類型的zval結構體保存之。
經過gdb調試,追查到number_format函數,在PHP底層最終會調用php_conf_fp函數對數字進行轉換:
函數原型以下:
PHPAPI char * php_conv_fp(register char format, register double num, boolean_e add_dp, int precision, char dec_point, bool_int * is_negative, char *buf, size_t *len);
這裏接收的參數num是一個double類型,所以,若是傳入的是字符串類型數字的話,number_format函數也會將其轉成double類型傳入到php_conf_fp函數裏。而這個double類型的num最終之因此輸出爲17978812896666957824,是由於進行科學計數法以後的精度丟失了,從新轉成double時就恢復不了原來的值。在C語言下驗證:
double local_dval = 1.7978812896666958E+19; printf("%f\n", local_dval);
輸出的結果就是
17978812896666957824.000000
因此,這不是PHP的bug,它就是這樣的。
對於存儲,超過PHP最大表示範圍的純整數,在MySQL中可使用bigint/varchar保存,MySQL在查詢出來的時候會將其使用string類型保存的。
對於賦值,在PHP裏,若是遇到有大整數須要賦值的話,不要嘗試用整型類型去賦值,好比,不要用如下這種:
$var = 17978812896666957068;
而用這種:
$var = '17978812896666957068';
而對於number_format,在64位操做系統下,它能解析的精度不會丟失的數,建議的最大值是這個:9007199254740991。參考鳥哥博客:http://www.laruence.com/2011/...
這個問題的緣由看起來不過重要,雖然學這個對於實際上的業務開發也沒什麼用,不會讓你的開發能力「duang"地一下上去幾個level,可是瞭解了PHP對於大整數的處理,也是本身知識框架的一個小小積累,知道了爲何以後,在平常開發中就會多加註意,好比從存儲以及使用賦值的角度。瞭解這個細節仍是頗有好處的。
回想整個解決問題的過程,我的感受有點長,總共大約花了4個小時去定位這個問題。由於對PHP的內核只是只知其一;不知其二,沒有系統的把整個流程梳理下來,因此一開始也不知道從哪裏開始下手,就開始根據本身的猜想來調試。如今回想起來,應該先學習PHP的編譯、執行流程,而後再去猜想具體的步驟。
原創文章,文筆有限,才疏學淺,文中如有不正之處,萬望告知。
若是本文對你有幫助,請點下推薦吧,謝謝^_^
更多精彩內容,請關注我的公衆號。