溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

怎么解決PHP大整數(shù)問題

發(fā)布時間:2021-11-19 17:09:52 來源:億速云 閱讀:219 作者:iii 欄目:編程語言

這篇文章主要介紹“怎么解決PHP大整數(shù)問題”,在日常操作中,相信很多人在怎么解決PHP大整數(shù)問題問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”怎么解決PHP大整數(shù)問題”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!

遇到的問題

最近遇到一個PHP大整數(shù)的問題,問題代碼是這樣的

$shopId = 17978812896666957068;  var_dump($shopId);

上面的代碼輸出,會把$shopId轉(zhuǎn)換成float類型,且使用了科學(xué)計數(shù)法來表示,輸出如下:

float(1.7978812896667E+19)

但在程序里需要的是完整的數(shù)字作為查找數(shù)據(jù)的參數(shù),所以需要用的是完整的數(shù)字,當(dāng)時以為只是因為數(shù)據(jù)被轉(zhuǎn)換成科學(xué)計數(shù)法了,于是想到的解決方案是強制讓它不使用科學(xué)計數(shù)法表示:

$shopId= number_format(17978812896666957068);  var_dump($shopId);

這時候奇怪的事情出現(xiàn)了,輸出的是:

17978812896666957824

當(dāng)時沒有仔細看,對比了前十位就沒有繼續(xù)往下看,所以認(rèn)為問題解決了,等到真正根據(jù)ID去找數(shù)據(jù)的時候才發(fā)現(xiàn)數(shù)據(jù)查不出來,這時候才發(fā)現(xiàn)是數(shù)據(jù)轉(zhuǎn)換錯誤了。

這里使用number_format失敗的原因在后面會講到,當(dāng)時就想到將原來的數(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是一門解釋型語言,所以當(dāng)時就大膽地猜測PHP是在編譯期間就將數(shù)字的字面量常量轉(zhuǎn)換成float類型,并用科學(xué)計數(shù)法表示。但僅僅猜測不能滿足自己的好奇心,想要看到真正實現(xiàn)代碼才愿意相信。于是就逐步分析、探索,直到找到背后的實現(xiàn)。

剛開始根據(jù)這個問題直接上網(wǎng)搜“PHP大整數(shù)解析過程”,并沒有搜到答案,因此只能自己去追查。一開始對PHP的執(zhí)行過程不熟悉,出發(fā)點就只能是一步一步地調(diào)試,然后

示例代碼:

// test.php $var = 17978812896666957068; var_dump($var);

追查過程

1、查看opcode

通過vld查看PHP執(zhí)行代碼的opcode,可以看到,賦值的是一個ASSIGN的opcode操作

怎么解決PHP大整數(shù)問題

接下來就想看看ASSIGN是在哪里執(zhí)行的。

2、gdb調(diào)試

2-1、用list查看有什么地方可以進行斷點

怎么解決PHP大整數(shù)問題

2-2、暫時沒有頭緒,在1186斷點試試

怎么解決PHP大整數(shù)問題

結(jié)果程序走到sapi/cli/php_cli.c文件的1200行了,按n不斷下一步執(zhí)行,一直到這里就走到了程序輸出結(jié)果了:

怎么解決PHP大整數(shù)問題

2-4、于是猜測,ASSIGN操作是在do_cli函數(shù)里面進行的,因此對do_cli函數(shù)做斷點:break do_cli。

輸入n,不斷回車,在sapi/cli/php_cli.c文件的993行之后就走到程序輸出結(jié)果了:

怎么解決PHP大整數(shù)問題

2-5、再對php_execute_script函數(shù)做斷點:break  php_execute_script,不斷逐步執(zhí)行,發(fā)現(xiàn)在main/main.c文件的2537行就走到程序輸出結(jié)果了:

怎么解決PHP大整數(shù)問題

2-6、繼續(xù)斷點的步驟:break  zend_execute_scripts,重復(fù)之前的步驟,發(fā)現(xiàn)在zend/Zend.c文件的1476行走到了程序輸出結(jié)果的步驟:

怎么解決PHP大整數(shù)問題

看到這里的時候,第1475行里有一個op_array,就猜測會不會是在op_array的時候就已經(jīng)有值了,于是開始打印op_array的值:

怎么解決PHP大整數(shù)問題

打印之后并沒有看到有用的信息,但是其實這里包含有很大的信息量,比如opcode的handler:  ZEND_ASSIGN_SPEC_CV_RETVAL_CV_CONST_RETVAL_UNUSED_HANDLER  ,但是當(dāng)時沒注意到,因此就想著看看op_array是怎么被賦值的,相關(guān)步驟做了什么。

2-7、重新從2-5的斷點開始,讓程序逐步執(zhí)行,看到op_array的賦值如下:

怎么解決PHP大整數(shù)問題

看到第1470行將zend_compile_file函數(shù)運行的結(jié)果賦值給op_array了,于是break  zend_compile_file,被告知zend_compile_file未定義,通過源碼工具追蹤到zend_compile_file指向的是compile_file,于是break  zend_compile

發(fā)現(xiàn)是在Zend/zend_language_scanner.l  文件斷點了,逐步執(zhí)行,看到這行pass_two(op_array),猜測可能會在這里就有值,所以打印看看:

怎么解決PHP大整數(shù)問題

結(jié)果發(fā)現(xiàn)還是跟之前的一樣,但是此時看到有一個opcodes的值,再打印看看

怎么解決PHP大整數(shù)問題

看到opcode = 38,網(wǎng)上查到38代表賦值

怎么解決PHP大整數(shù)問題

2-8、于是可以知道,在這一步之前就已得到了ASSIGN的opcode,因此,不斷地往前找,從op_array開始初始化時就開始,逐步打印op_array->opcodes的值,一直都是null,

怎么解決PHP大整數(shù)問題

直到執(zhí)行了 CG(zend_lineno) = last_lineno; 才得到opcode = 38 的值:

怎么解決PHP大整數(shù)問題

因為這一句:CG(zend_lineno) = last_lineno;是一個宏,所以也沒頭緒,接近放棄狀態(tài)。。。

于是先去了解opcode的數(shù)據(jù)結(jié)構(gòu),在 深入理解PHP內(nèi)核書 里找到opcode處理函數(shù)查找這一章,給了我一些繼續(xù)下去的思路。

引用里面的內(nèi)容:

在PHP內(nèi)部有一個函數(shù)用來快速的返回特定opcode對應(yīng)的opcode處理函數(shù)指針:zend_vm_get_opcode_handler()函數(shù):

怎么解決PHP大整數(shù)問題

知道其實opcode處理函數(shù)的命名是有以下規(guī)律的

ZEND_[opcode]_SPEC_(變量類型1)_(變量類型2)_HANDLER

根據(jù)之前調(diào)試打印出來的內(nèi)容,在2-6的時候就看到了一個handler的值:

怎么解決PHP大整數(shù)問題

ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER,

找出函數(shù)的定義如下:

怎么解決PHP大整數(shù)問題

可以看到,opcode操作的時候,值是從EX_CONSTANT獲取的,根據(jù)定義展開這個宏,那就是

opline->op2->execute_data->literals

這里可以得到兩個信息:

  1. 參數(shù)的轉(zhuǎn)換在opcode執(zhí)行前就做好了

  2. 賦值過程取值時是在op2->execute_data->literals,如果猜想沒錯的話,op2->execute_data->literals此時保存的就是格式轉(zhuǎn)換后的值,可以打印出來驗證一下

打印結(jié)果如下:

怎么解決PHP大整數(shù)問題

猜想驗證正確,但是沒有看到真正做轉(zhuǎn)換的地方,還是不死心,繼續(xù)找PHP的Zend底層做編譯的邏輯代碼。

參考開源的 GitHub項目 ,PHP編譯階段如下圖:

怎么解決PHP大整數(shù)問題

猜測最有可能的是在zendparse、zend_compile_top_stmt這兩個階段完成轉(zhuǎn)換,因為這個兩個階段做的事情就是將PHP代碼轉(zhuǎn)換成opcode數(shù)組。

上網(wǎng)搜索了PHP語法分析相關(guān)的文章,有一篇里面講到了解析整數(shù)的過程,因此找到了PHP真正將大整數(shù)做轉(zhuǎn)換的地方:

<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代碼在對純數(shù)字的表達式做詞法分析的時候,先判斷數(shù)字是否有可能會溢出,如果有可能溢出,先嘗試將其用LONG類型保存,如果溢出,先用zend_strtod將其轉(zhuǎn)換為double類型,然后用double類型的zval結(jié)構(gòu)體保存之。

number_format失敗的原因

通過gdb調(diào)試,追查到number_format函數(shù),在PHP底層最終會調(diào)用php_conv_fp函數(shù)對數(shù)字進行轉(zhuǎn)換:

怎么解決PHP大整數(shù)問題

函數(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是一個double類型,因此,如果傳入的是字符串類型數(shù)字的話,number_format函數(shù)也會將其轉(zhuǎn)成double類型傳入到php_conf_fp函數(shù)里。而這個double類型的num最終之所以輸出為17978812896666957824,是因為進行科學(xué)計數(shù)法之后的精度丟失了,重新轉(zhuǎn)成double時就恢復(fù)不了原來的值。在C語言下驗證:

double local_dval = 1.7978812896666958E+19; printf("%f\n", local_dval);

輸出的結(jié)果就是

17978812896666957824.000000

所以,這不是PHP的bug,它就是這樣的。

此類問題解決方案

對于存儲,超過PHP***表示范圍的純整數(shù),在MySQL中可以使用bigint/varchar保存,MySQL在查詢出來的時候會將其使用string類型保存的。

對于賦值,在PHP里,如果遇到有大整數(shù)需要賦值的話,不要嘗試用整型類型去賦值,比如,不要用以下這種:

$var = 17978812896666957068;

而用這種:

$var = '17978812896666957068';

而對于number_format,在64位操作系統(tǒng)下,它能解析的精度不會丟失的數(shù),建議的***值是這個:9007199254740991。

到此,關(guān)于“怎么解決PHP大整數(shù)問題”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

php
AI