摘要:而這個(gè)類型的最終之所以輸出為,是因?yàn)檫M(jìn)行科學(xué)計(jì)數(shù)法之后的精度丟失了,重新轉(zhuǎn)成時(shí)就恢復(fù)不了原來(lái)的值。此類問(wèn)題解決方案對(duì)于存儲(chǔ),超過(guò)最大表示范圍的純整數(shù),在中可以使用保存,在查詢出來(lái)的時(shí)候會(huì)將其使用類型保存的。
遇到的問(wèn)題
最近遇到一個(gè)PHP大整數(shù)的問(wèn)題,問(wèn)題代碼是這樣的
$shopId = 17978812896666957068; var_dump($shopId);
上面的代碼輸出,會(huì)把$shopId轉(zhuǎn)換成float類型,且使用了科學(xué)計(jì)數(shù)法來(lái)表示,輸出如下:
float(1.7978812896667E+19)
但在程序里需要的是完整的數(shù)字作為查找數(shù)據(jù)的參數(shù),所以需要用的是完整的數(shù)字,當(dāng)時(shí)以為只是因?yàn)閿?shù)據(jù)被轉(zhuǎn)換成科學(xué)計(jì)數(shù)法了,于是想到的解決方案是強(qiáng)制讓它不使用科學(xué)計(jì)數(shù)法表示:
$shopId= number_format(17978812896666957068); var_dump($shopId);
這時(shí)候奇怪的事情出現(xiàn)了,輸出的是:
17978812896666957824
當(dāng)時(shí)沒(méi)有仔細(xì)看,對(duì)比了前十位就沒(méi)有繼續(xù)往下看,所以認(rèn)為問(wèn)題解決了,等到真正根據(jù)ID去找數(shù)據(jù)的時(shí)候才發(fā)現(xiàn)數(shù)據(jù)查不出來(lái),這時(shí)候才發(fā)現(xiàn)是數(shù)據(jù)轉(zhuǎn)換錯(cuò)誤了。
這里使用number_format失敗的原因在后面會(huì)講到,當(dāng)時(shí)就想到將原來(lái)的數(shù)據(jù)轉(zhuǎn)成字符串的,但是使用了以下方法仍然不行
$shopId= strval(17978812896666957068); var_dump($shopId); $shopId = 17978812896666957068 . ‘’; var_dump($shopId);
輸出的結(jié)果都是
float(1.7978812896667E+19)
最后只有下面這種方案是可行的:
$shopId = ‘17978812896666957068’; var_dump($shopId); // 輸出 //string(20) "17978812896666957068"
眾所周知,PHP是一門解釋型語(yǔ)言,所以當(dāng)時(shí)就大膽地猜測(cè)PHP是在編譯期間就將數(shù)字的字面量常量轉(zhuǎn)換成float類型,并用科學(xué)計(jì)數(shù)法表示。但僅僅猜測(cè)不能滿足自己的好奇心,想要看到真正實(shí)現(xiàn)代碼才愿意相信。于是就逐步分析、探索,直到找到背后的實(shí)現(xiàn)。
剛開(kāi)始根據(jù)這個(gè)問(wèn)題直接上網(wǎng)搜“PHP大整數(shù)解析過(guò)程”,并沒(méi)有搜到答案,因此只能自己去追查。一開(kāi)始對(duì)PHP的執(zhí)行過(guò)程不熟悉,出發(fā)點(diǎn)就只能是一步一步地調(diào)試,然后
示例代碼:
// test.php $var = 17978812896666957068; var_dump($var);追查過(guò)程
1、查看opcode
通過(guò)vld查看PHP執(zhí)行代碼的opcode,可以看到,賦值的是一個(gè)ASSIGN的opcode操作
接下來(lái)就想看看ASSIGN是在哪里執(zhí)行的。
2、gdb調(diào)試
2-1、用list查看有什么地方可以進(jìn)行斷點(diǎn)
2-2、暫時(shí)沒(méi)有頭緒,在1186斷點(diǎn)試試
結(jié)果程序走到sapi/cli/php_cli.c文件的1200行了,按n不斷下一步執(zhí)行,一直到這里就走到了程序輸出結(jié)果了:
2-4、于是可以猜測(cè),ASSIGN操作是在do_cli函數(shù)里面進(jìn)行的,因此對(duì)do_cli函數(shù)做斷點(diǎn):break do_cli。
輸入n,不斷回車,在sapi/cli/php_cli.c文件的993行之后就走到程序輸出結(jié)果了:
2-5、再對(duì)php_execute_script函數(shù)做斷點(diǎn):break php_execute_script
不斷逐步執(zhí)行,發(fā)現(xiàn)在main/main.c文件的2537行就走到程序輸出結(jié)果了:
2-6、繼續(xù)斷點(diǎn)的步驟:break zend_execute_scripts
重復(fù)之前的步驟,發(fā)現(xiàn)在zend/Zend.c文件的1476行走到了程序輸出結(jié)果的步驟:
看到這里的時(shí)候,第1475行里有一個(gè)op_array,就猜測(cè)會(huì)不會(huì)是在op_array的時(shí)候就已經(jīng)有值了,于是開(kāi)始打印op_array的值:
打印之后并沒(méi)有看到有用的信息,但是其實(shí)這里包含有很大的信息量,比如opcode的handler: ZEND_ASSIGN_SPEC_CV_RETVAL_CV_CONST_RETVAL_UNUSED_HANDLER,但是當(dāng)時(shí)沒(méi)注意到,因此就想著看看op_array是怎么被賦值的,相關(guān)步驟做了什么。
2-7、重新從2-5的斷點(diǎn)開(kāi)始,讓程序逐步執(zhí)行,看到op_array的賦值如下:
將zend_compile_file函數(shù)運(yùn)行的結(jié)果賦值給op_array了,于是break zend_compile_file,被告知zend_compile_file未定義,通過(guò)源碼工具追蹤到zend_compile_file指向的是compile_file,于是break zend_compile
發(fā)現(xiàn)是在Zend/zend_language_scanner.l 文件斷點(diǎn)了,逐步執(zhí)行,看到這行pass_two(op_array),猜測(cè)可能會(huì)在這里就有值,所以打印看看:
結(jié)果發(fā)現(xiàn)還是跟之前的一樣,但是此時(shí)看到有一個(gè)opcodes的值,打印看看
看到opcode = 38,網(wǎng)上查到38代表賦值
2-8、于是可以知道,在這一步之前就已得到了ASSIGN的opcode,因此,不斷的往前找,從op_array開(kāi)始初始化時(shí)就開(kāi)始,逐步打印op_array->opcodes的值,一直都是null:
直到執(zhí)行了CG(zend_lineno) = last_lineno;才得到opcode = 38 的值:
因?yàn)檫@一句:CG(zend_lineno) = last_lineno;是一個(gè)宏,所以也沒(méi)頭緒,接近放棄狀態(tài)。。。
于是先去了解opcode的數(shù)據(jù)結(jié)構(gòu),在深入理解PHP內(nèi)核書(shū)里找到opcode處理函數(shù)查找這一章,給了我一些繼續(xù)下去的思路。
引用里面的內(nèi)容:
在PHP內(nèi)部有一個(gè)函數(shù)用來(lái)快速的返回特定opcode對(duì)應(yīng)的opcode處理函數(shù)指針:zend_vm_get_opcode_handler()函數(shù):
知道其實(shí)opcode處理函數(shù)的命名是有以下規(guī)律的
ZEND_[opcode]_SPEC_(變量類型1)_(變量類型2)_HANDLER
根據(jù)之前調(diào)試打印出來(lái)的內(nèi)容,在2-6的時(shí)候就看到了一個(gè)handler的值:
是
ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER,
找出函數(shù)的定義如下:
可以看到,opcode操作的時(shí)候,值是從EX_CONSTANT獲取的,根據(jù)定義展開(kāi)這個(gè)宏,那就是
opline->op2->execute_data->literals
這里可以得到兩個(gè)信息:
1、參數(shù)的轉(zhuǎn)換在opcode執(zhí)行前就做好了
2、賦值過(guò)程取值時(shí)是在op2->execute_data->literals,如果猜想沒(méi)錯(cuò)的話,op2->execute_data->literals此時(shí)保存的就是格式轉(zhuǎn)換后的值,可以打印出來(lái)驗(yàn)證一下
打印結(jié)果如下:
猜想驗(yàn)證正確,但是沒(méi)有看到真正做轉(zhuǎn)換的地方,還是不死心,繼續(xù)找PHP的Zend底層做編譯的邏輯代碼。
參考開(kāi)源的GitHub項(xiàng)目,PHP編譯階段如下圖:
猜測(cè)最有可能的是在zendparse、zend_compile_top_stmt這兩個(gè)階段完成轉(zhuǎn)換,因?yàn)檫@個(gè)兩個(gè)階段做的事情就是將PHP代碼轉(zhuǎn)換成opcode數(shù)組。
上網(wǎng)搜索了PHP語(yǔ)法分析相關(guān)的文章,有一篇里面講到了解析整數(shù)的過(guò)程,因此找到了PHP真正將大整數(shù)做轉(zhuǎn)換的地方:
{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引擎在對(duì)PHP代碼在對(duì)純數(shù)字的表達(dá)式做詞法分析的時(shí)候,先判斷數(shù)字是否有可能會(huì)溢出,如果有可能溢出,先嘗試將其用LONG類型保存,如果溢出,先用zend_strtod將其轉(zhuǎn)換為double類型,然后用double類型的zval結(jié)構(gòu)體保存之。
number_format失敗的原因通過(guò)gdb調(diào)試,追查到number_format函數(shù),在PHP底層最終會(huì)調(diào)用php_conf_fp函數(shù)對(duì)數(shù)字進(jìn)行轉(zhuǎn)換:
函數(shù)原型如下:
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);
這里接收的參數(shù)num是一個(gè)double類型,因此,如果傳入的是字符串類型數(shù)字的話,number_format函數(shù)也會(huì)將其轉(zhuǎn)成double類型傳入到php_conf_fp函數(shù)里。而這個(gè)double類型的num最終之所以輸出為17978812896666957824,是因?yàn)檫M(jìn)行科學(xué)計(jì)數(shù)法之后的精度丟失了,重新轉(zhuǎn)成double時(shí)就恢復(fù)不了原來(lái)的值。在C語(yǔ)言下驗(yàn)證:
double local_dval = 1.7978812896666958E+19; printf("%f ", local_dval);
輸出的結(jié)果就是
17978812896666957824.000000
所以,這不是PHP的bug,它就是這樣的。
此類問(wèn)題解決方案對(duì)于存儲(chǔ),超過(guò)PHP最大表示范圍的純整數(shù),在MySQL中可以使用bigint/varchar保存,MySQL在查詢出來(lái)的時(shí)候會(huì)將其使用string類型保存的。
對(duì)于賦值,在PHP里,如果遇到有大整數(shù)需要賦值的話,不要嘗試用整型類型去賦值,比如,不要用以下這種:
$var = 17978812896666957068;
而用這種:
$var = "17978812896666957068";
而對(duì)于number_format,在64位操作系統(tǒng)下,它能解析的精度不會(huì)丟失的數(shù),建議的最大值是這個(gè):9007199254740991。參考鳥(niǎo)哥博客:http://www.laruence.com/2011/...
總結(jié)這個(gè)問(wèn)題的原因看起來(lái)不太重要,雖然學(xué)這個(gè)對(duì)于實(shí)際上的業(yè)務(wù)開(kāi)發(fā)也沒(méi)什么用,不會(huì)讓你的開(kāi)發(fā)能力“duang"地一下上去幾個(gè)level,但是了解了PHP對(duì)于大整數(shù)的處理,也是自己知識(shí)框架的一個(gè)小小積累,知道了為什么之后,在日常開(kāi)發(fā)中就會(huì)多加注意,比如從存儲(chǔ)以及使用賦值的角度。了解這個(gè)細(xì)節(jié)還是很有好處的。
回想整個(gè)解決問(wèn)題的過(guò)程,個(gè)人感覺(jué)有點(diǎn)長(zhǎng),總共大約花了4個(gè)小時(shí)去定位這個(gè)問(wèn)題。因?yàn)閷?duì)PHP的內(nèi)核只是一知半解,沒(méi)有系統(tǒng)的把整個(gè)流程梳理下來(lái),所以一開(kāi)始也不知道從哪里開(kāi)始下手,就開(kāi)始根據(jù)自己的猜測(cè)來(lái)調(diào)試?,F(xiàn)在回想起來(lái),應(yīng)該先學(xué)習(xí)PHP的編譯、執(zhí)行流程,然后再去猜測(cè)具體的步驟。
原創(chuàng)文章,文筆有限,才疏學(xué)淺,文中若有不正之處,萬(wàn)望告知。
如果本文對(duì)你有幫助,請(qǐng)點(diǎn)下推薦吧,謝謝^_^
更多精彩內(nèi)容,請(qǐng)關(guān)注個(gè)人公眾號(hào)。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/25845.html
摘要:可以將其他類型轉(zhuǎn)成字符串函數(shù)可以將任意類型的值轉(zhuǎn)為布爾值。提示空數(shù)組空對(duì)象轉(zhuǎn)換為布爾型也是。 在JS中數(shù)據(jù)類型轉(zhuǎn)換有兩種 相關(guān)資料參閱 官方ecma-5規(guī)范阮一峰老師類型轉(zhuǎn)換規(guī)范對(duì)相等==定義 強(qiáng)制類型轉(zhuǎn)換Number() Number函數(shù)將字符串轉(zhuǎn)為數(shù)值,要比parseInt函數(shù)嚴(yán)格很多,只要有一個(gè)字符是非數(shù)字(空格、+、-除外),那么就會(huì)被轉(zhuǎn)為NaN。 showImg(http...
摘要:服務(wù)化的出現(xiàn)假想一個(gè)京東的發(fā)展路程都是我虛構(gòu)的。更多的服務(wù)的提取抽離,更多的團(tuán)隊(duì)出現(xiàn)業(yè)務(wù)繼續(xù)發(fā)展,出現(xiàn)了京東大藥房,專門賣藥,需要調(diào)用京東目前的財(cái)務(wù)系統(tǒng)。 以下內(nèi)容都是自己的理解,不保證正確,可能是對(duì)的,也可能把你帶溝里,自己甄別。 更多詳情請(qǐng)看直播 揭開(kāi)她的神秘面紗 - 零基礎(chǔ)構(gòu)建自己的服務(wù)治理框架 https://segmentfault.com/l/15... 很久之前聽(tīng)別人分...
摘要:一般流程你可以通過(guò)良好的選擇一種構(gòu)造來(lái)設(shè)計(jì)你自己的文件領(lǐng)域。 隨著云世界的日新月異,想進(jìn)入云世界的您在面對(duì)大量的選擇時(shí)該何去何從?下面是一些關(guān)于知名云服務(wù)的使用和測(cè)試總結(jié)。當(dāng)然在這之前,我們必須知道云是建立在托管您的應(yīng)用程序上的服務(wù)。 Auto-Scalling — 當(dāng)需求增加時(shí),你可以自動(dòng)的獲得更多資源(一般情況下是更多的虛擬機(jī))來(lái)響應(yīng)請(qǐng)求。這在一般的應(yīng)用程序中很少用到,但能保證你的服...
摘要:在看這篇長(zhǎng)輪詢之前可以先看看輪詢技術(shù)沒(méi)有長(zhǎng),有助于理解長(zhǎng)輪詢屬于輪詢的升級(jí)版,在客戶端和服務(wù)端都進(jìn)行了一些改造,使得消耗更低,速度更快。不間斷的通過(guò)查詢服務(wù)端。然后客戶端不間斷繼續(xù)發(fā)起請(qǐng)求數(shù)據(jù)不存在,繼續(xù)循環(huán)。 在看這篇Ajax長(zhǎng)輪詢之前可以先看看Ajax輪詢技術(shù)(沒(méi)有長(zhǎng)),有助于理解: Ajax長(zhǎng)輪詢屬于Ajax輪詢的升級(jí)版,在客戶端和服務(wù)端都進(jìn)行了一些改造,使得消耗更低,速度更快。...
閱讀 3886·2021-11-25 09:43
閱讀 2282·2021-11-23 10:13
閱讀 903·2021-11-16 11:44
閱讀 2433·2019-08-29 17:24
閱讀 1456·2019-08-29 17:17
閱讀 3537·2019-08-29 11:30
閱讀 2652·2019-08-26 13:23
閱讀 2409·2019-08-26 12:10