Jadual Kandungan
ceil 会返回 -0.0
不能直接传入 -0.0
直接调用或赋值给变量
最后的原因
后话
Rumah pembangunan bahagian belakang tutorial php max/min 函数(PHP)的一个小 BUG

max/min 函数(PHP)的一个小 BUG

Jun 23, 2016 pm 01:23 PM

先直接来看一段展示:

# Psy Shell v0.3.3 (PHP 5.5.30 — cli) by Justin Hileman>>> ceil(-0.5)=> -0.0>>> max(-0.0, 0)=> 0.0>>> max(ceil(-0.5), 0)=> -0.0
Salin selepas log masuk

上面的演示中,ceil 函数返回的是 -0.0,max 在将 ceil 函数调用的结果作为参数传入的时候,返回的也是一个 -0.0。

如果给 ceil 的结果赋值给变量,还是能得到 -0.0 的结果:

>>> $a = ceil(-0.5)=> -0.0>>> max($a, 0)=> -0.0
Salin selepas log masuk

下面就来一一分析是哪些原因导致了这些结果的产生。

ceil 会返回 -0.0

首先我们来看一下为什么 ceil 函数会返回 -0.0。

ceil 函数的实现在 $PHP-SRC/ext/stardands/math.c ($PHP-SRC 指的是 PHP 解释器源码根目录)中,为了展示清楚我去掉了一些细节:

PHP_FUNCTION(ceil){    ...    if (Z_TYPE_PP(value) == IS_DOUBLE) {        RETURN_DOUBLE(ceil(Z_DVAL_PP(value)));    } else if (Z_TYPE_PP(value) == IS_LONG) {        convert_to_double_ex(value);        RETURN_DOUBLE(Z_DVAL_PP(value));    }    ...}
Salin selepas log masuk

从这里可以看出来 ceil 函数做了两个事情:

  • 如果参数类型是 double,则直接调用 C 语言的 ceil 函数并返回执行结果;

  • 如果参数类型是 long,则转换成 double 然后直接返回。

  • 所以 ceil 返回 -0.0 这个本身的原因还在于 C。写个函数测试一下:

    #include <stdio.h>#include <math.h>int main(int argc, char const *argv[]){    printf("%f\n", ceil(-0.5));    return 0;}
    Salin selepas log masuk

    以上代码在我机器上的执行结果是 -0.000000。至于为什么会是这个结果,这是 C 语言的问题,这里也不细说,有兴趣的可以看这里:http://www.wikiwand.com/zh/-0。

    不能直接传入 -0.0

    接下来讨论一下为什么执行 max(-0.0, 0) 却得不到相同的结果。

    用 vld 扩展查看了一下只有以上一行代码的 php 文件看一下结果:

    line     #* E I O op                    fetch      ext  return  operands--------------------------------------------------------------------------   3     0  E >   EXT_STMT         1        EXT_FCALL_BEGIN         2        SEND_VAL                                      0         3        SEND_VAL                                      0         4        DO_FCALL                           2          'max'         5        EXT_FCALL_END   5     6      > RETURN                                        1
    Salin selepas log masuk

    注意到需要为 2 的 SEND_VAL 操作,送进去的值是 0。也就说在词法分析阶段之后 -0.0 就被转换成 0 了。如何转换的呢?下面我们简单的分析一下的过程。

    PHP 的词法分析器由 re2c 生成,语法分析器则是由 Bison 生成。在 zend_language_scanner.l ($PHP-SRC/Zend 目录下)中我们可以找到以下的语句:

    LNUM    [0-9]+DNUM    ([0-9]*"."[0-9]+)|([0-9]+"."[0-9]*)EXPONENT_DNUM    (({LNUM}|{DNUM})[eE][+-]?{LNUM})......<ST_IN_SCRIPTING>{DNUM}|{EXPONENT_DNUM} {    zendlval->value.dval = zend_strtod(yytext, NULL);    zendlval->type = IS_DOUBLE;    return T_DNUMBER;}
    Salin selepas log masuk

    LNUM 和 DNUM 后面都是简单的正则表达式。虽然在词法扫描中 0.0 会被标记成 DNUM,并且位于 zend_strtod.c zend_strtod 函数中的也有对于 加减号的处理,但是 - 符号并不和 DNUM 匹配(那既然这样为什么 zend_strtod 还要处理加减号呢?因为这个函数不只是在这里使用的)。这里最终返回一个 T_DNUMBER 的标记。

    再看 zend_language_parser.y 中:

    common_scalar:        T_LNUMBER                     { $$ = $1; }    |    T_DNUMBER                     { $$ = $1; }    ...;static_scalar: /* compile-time evaluated scalars */        common_scalar        { $$ = $1; }    ...    |    '+' static_scalar { ZVAL_LONG(&$1.u.constant, 0); add_function(&$2.u.constant, &$1.u.constant, &$2.u.constant TSRMLS_CC); $$ = $2; }    |    '-' static_scalar { ZVAL_LONG(&$1.u.constant, 0); sub_function(&$2.u.constant, &$1.u.constant, &$2.u.constant TSRMLS_CC); $$ = $2; }
    Salin selepas log masuk

    同样我们去掉了一些细节,简单描述一下上面的语法分析的处理流程:

  • T_DNUMBER 是一个 common_scalar 语句;

  • common_scalar 是一个 static_scalar 语句;

  • static_scalar 语句前面存在减号时,将操作数 1 (op1)设定为 值为 0 的 ZVAL_LONG ,然后调用 sub_function 函数处理两个操作数。

  • sub_function 函数的实现位于 zend_operators.c 中,所做的操作很简单,就是用 op1 的值减去 op2 的值,所以就不会存在传入 -0.0 的情况。

    直接调用或赋值给变量

    既然如此,为什么直接使用函数调用做参数或者赋值给变量的方式又可以传入呢?闲来看一下 zend_language_parser.y 中对于函数参数的分析语句:

    function_call_parameter_list:        '(' ')'    { Z_LVAL($$.u.constant) = 0; }    |    '(' non_empty_function_call_parameter_list ')'    { $$ = $2; }    |    '(' yield_expr ')'    { Z_LVAL($$.u.constant) = 1; zend_do_pass_param(&$2, ZEND_SEND_VAL, Z_LVAL($$.u.constant) TSRMLS_CC); };non_empty_function_call_parameter_list:        expr_without_variable    { Z_LVAL($$.u.constant) = 1;  zend_do_pass_param(&$1, ZEND_SEND_VAL, Z_LVAL($$.u.constant) TSRMLS_CC); }    |    variable                { Z_LVAL($$.u.constant) = 1;  zend_do_pass_param(&$1, ZEND_SEND_VAR, Z_LVAL($$.u.constant) TSRMLS_CC); }    |    '&' w_variable                 { Z_LVAL($$.u.constant) = 1;  zend_do_pass_param(&$2, ZEND_SEND_REF, Z_LVAL($$.u.constant) TSRMLS_CC); }...;
    Salin selepas log masuk

    为了直观 non_empty_function_call_parameter_list 语句块后面我隐去了三行。后面三行的处理逻辑实际上是递归调用,并不影响我们分析。

    通过 function_call_parameter_list 可以看出函数的参数基本情况包括三种:

  • 没有参数

  • 有参数列表

  • 有 yield 表达式

  • 这里我们只需要关注有参数列表的情况,参数列表中的每个参数也分三种情况:

  • 不包含变量的表达式

  • 变量

  • 引用变量

  • 上文中我们提到的直接传入 -0.0 时对应的是第一种情况,传入赋值后的 $a 对应的是第二种情况。参数最终都会交给 zend_do_pass_param 函数(zend_compile.c)去处理。

    那么传入 ceil(-0.5) 作为参数呢?实际上也是对应第二种情况,这个问题单独分析起来也比较复杂,省事儿一点我们直接用 vld 看一下执行 max(ceil(-0.5), 0)过程:

    line     #* E I O op                   fetch       ext  return  operands--------------------------------------------------------------------------   5     0  E >   EXT_STMT         1        EXT_FCALL_BEGIN         2        EXT_FCALL_BEGIN         3        SEND_VAL                                      -0.5         4        DO_FCALL                           1  $0      'ceil'         5        EXT_FCALL_END         6        SEND_VAR_NO_REF                    6          $0         7        SEND_VAL                                      0         8        DO_FCALL                           2          'max'         9        EXT_FCALL_END   6    10      > RETURN                                        1
    Salin selepas log masuk

    序号为 4 的语句中,ceil 的执行结果是赋值给一个 $0 的变量,而在序号为 6 的执行中,执行的是 SEND_VAR_NO_REF 的语句,调用的 $0。SEND_VAR_NO_REF 的 Opcode 是在何时被指定的呢?也是在 zend_do_pass_param 函数中:

    if (op == ZEND_SEND_VAR && zend_is_function_or_method_call(param)) {    /* Method call */    op = ZEND_SEND_VAR_NO_REF;    ...}
    Salin selepas log masuk

    函数执行过程中使用 zend_parse_parameters 函数(zend_API.c)来获取参数。从参数的存储到获取中间还有很多处理过程,这里不再一一详解。但是需要知道一件事:函数在使用变量作为参数的时候是直接从已经存储的变量列表中读取的,没有经过过滤处理,所以变量 $a 或 ceil(-0.5) 才可以直接将 -0.0 传递给 max 函数使用。

    最后的原因

    既然以上都知道了,那还剩一个问题:为什么在 -0.0 和 0 中 max 函数会选择前者?

    其实这个问题很简单,看一下 max 函数的实现($PHP-SRC/ext/standard/array.c)就知道真的就是在两值相等时选择了前者:

    max = args[0];for (i = 1; i < argc; i++) {    is_smaller_or_equal_function(&result, *args[i], *max TSRMLS_CC);    if (Z_LVAL(result) == 0) {        max = args[i];    }}
    Salin selepas log masuk

    同样,min 函数也存在这个问题,区别就是 min 函数是调用的 is_smaller_function 来比较两个数值,两个值相等的时候返回前者。

    所以要解决这个问题也很简单,只需要调换一下参数顺序即可:

    # Psy Shell v0.3.3 (PHP 5.5.30 — cli) by Justin Hileman>>> max(0, ceil(-0.5))=> 0
    Salin selepas log masuk

    后话

    本文仅仅是管中窥豹,从一个小 “bug” 入口简单的梳理一下各个环节的处理过程,如果想要更深入的理解 PHP 的执行过程,还需要大量的精力和知识储备。

    分析 PHP 源码的执行过程不仅是为了对 PHP 有更深刻的理解,也能帮助我们了解一门语言从代码到执行结果中间的各个环节和实现。

    关于词法分析器与语法分析器,这里讲的并不多,希望后面有机会的话能够再深入探讨。re2c 的规则比较简单,关于 Bison,则有很多相关的书籍。

    文中有粗浅的疏解,也留下有问题,如有错误,欢迎指正。

    Stay foolish,stay humble; Keep questioning,keep learning.

    私博地址:http://0x1.im

    Kenyataan Laman Web ini
    Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn

    Alat AI Hot

    Undresser.AI Undress

    Undresser.AI Undress

    Apl berkuasa AI untuk mencipta foto bogel yang realistik

    AI Clothes Remover

    AI Clothes Remover

    Alat AI dalam talian untuk mengeluarkan pakaian daripada foto.

    Undress AI Tool

    Undress AI Tool

    Gambar buka pakaian secara percuma

    Clothoff.io

    Clothoff.io

    Penyingkiran pakaian AI

    AI Hentai Generator

    AI Hentai Generator

    Menjana ai hentai secara percuma.

    Artikel Panas

    R.E.P.O. Kristal tenaga dijelaskan dan apa yang mereka lakukan (kristal kuning)
    1 bulan yang lalu By 尊渡假赌尊渡假赌尊渡假赌
    R.E.P.O. Tetapan grafik terbaik
    1 bulan yang lalu By 尊渡假赌尊渡假赌尊渡假赌
    Akan R.E.P.O. Ada Crossplay?
    1 bulan yang lalu By 尊渡假赌尊渡假赌尊渡假赌

    Alat panas

    Notepad++7.3.1

    Notepad++7.3.1

    Editor kod yang mudah digunakan dan percuma

    SublimeText3 versi Cina

    SublimeText3 versi Cina

    Versi Cina, sangat mudah digunakan

    Hantar Studio 13.0.1

    Hantar Studio 13.0.1

    Persekitaran pembangunan bersepadu PHP yang berkuasa

    Dreamweaver CS6

    Dreamweaver CS6

    Alat pembangunan web visual

    SublimeText3 versi Mac

    SublimeText3 versi Mac

    Perisian penyuntingan kod peringkat Tuhan (SublimeText3)

    Jelaskan JSON Web Tokens (JWT) dan kes penggunaannya dalam PHP API. Jelaskan JSON Web Tokens (JWT) dan kes penggunaannya dalam PHP API. Apr 05, 2025 am 12:04 AM

    JWT adalah standard terbuka berdasarkan JSON, yang digunakan untuk menghantar maklumat secara selamat antara pihak, terutamanya untuk pengesahan identiti dan pertukaran maklumat. 1. JWT terdiri daripada tiga bahagian: header, muatan dan tandatangan. 2. Prinsip kerja JWT termasuk tiga langkah: menjana JWT, mengesahkan JWT dan muatan parsing. 3. Apabila menggunakan JWT untuk pengesahan di PHP, JWT boleh dijana dan disahkan, dan peranan pengguna dan maklumat kebenaran boleh dimasukkan dalam penggunaan lanjutan. 4. Kesilapan umum termasuk kegagalan pengesahan tandatangan, tamat tempoh, dan muatan besar. Kemahiran penyahpepijatan termasuk menggunakan alat debugging dan pembalakan. 5. Pengoptimuman prestasi dan amalan terbaik termasuk menggunakan algoritma tandatangan yang sesuai, menetapkan tempoh kesahihan dengan munasabah,

    Huraikan prinsip -prinsip yang kukuh dan bagaimana ia memohon kepada pembangunan PHP. Huraikan prinsip -prinsip yang kukuh dan bagaimana ia memohon kepada pembangunan PHP. Apr 03, 2025 am 12:04 AM

    Penerapan prinsip pepejal dalam pembangunan PHP termasuk: 1. Prinsip Tanggungjawab Tunggal (SRP): Setiap kelas bertanggungjawab untuk hanya satu fungsi. 2. Prinsip Terbuka dan Tutup (OCP): Perubahan dicapai melalui lanjutan dan bukannya pengubahsuaian. 3. Prinsip Penggantian Lisch (LSP): Subkelas boleh menggantikan kelas asas tanpa menjejaskan ketepatan program. 4. Prinsip Pengasingan Antara Muka (ISP): Gunakan antara muka halus untuk mengelakkan kebergantungan dan kaedah yang tidak digunakan. 5. Prinsip Inversi Ketergantungan (DIP): Modul peringkat tinggi dan rendah bergantung kepada abstraksi dan dilaksanakan melalui suntikan ketergantungan.

    Terangkan konsep pengikatan statik lewat dalam PHP. Terangkan konsep pengikatan statik lewat dalam PHP. Mar 21, 2025 pm 01:33 PM

    Artikel membincangkan pengikatan statik lewat (LSB) dalam PHP, yang diperkenalkan dalam Php 5.3, yang membolehkan resolusi runtime kaedah statik memerlukan lebih banyak warisan yang fleksibel. Isu: LSB vs polimorfisme tradisional; Aplikasi Praktikal LSB dan Potensi Perfo

    Bagaimana cara menghantar permintaan pos yang mengandungi data JSON menggunakan perpustakaan php curl? Bagaimana cara menghantar permintaan pos yang mengandungi data JSON menggunakan perpustakaan php curl? Apr 01, 2025 pm 03:12 PM

    Menghantar data JSON menggunakan perpustakaan Curl PHP dalam pembangunan PHP, sering kali perlu berinteraksi dengan API luaran. Salah satu cara biasa ialah menggunakan perpustakaan curl untuk menghantar post ...

    Ciri -ciri Keselamatan Rangka Kerja: Melindungi Kelemahan. Ciri -ciri Keselamatan Rangka Kerja: Melindungi Kelemahan. Mar 28, 2025 pm 05:11 PM

    Artikel membincangkan ciri -ciri keselamatan penting dalam rangka kerja untuk melindungi daripada kelemahan, termasuk pengesahan input, pengesahan, dan kemas kini tetap.

    Bagaimana cara menetapkan kebenaran secara automatik UnixSocket selepas sistem dimulakan semula? Bagaimana cara menetapkan kebenaran secara automatik UnixSocket selepas sistem dimulakan semula? Mar 31, 2025 pm 11:54 PM

    Bagaimana untuk menetapkan keizinan UnixSocket secara automatik selepas sistem dimulakan semula. Setiap kali sistem dimulakan semula, kita perlu melaksanakan perintah berikut untuk mengubahsuai keizinan UnixSocket: sudo ...

    Menyesuaikan/Memperluas Rangka Kerja: Cara Menambah Fungsi Custom. Menyesuaikan/Memperluas Rangka Kerja: Cara Menambah Fungsi Custom. Mar 28, 2025 pm 05:12 PM

    Artikel ini membincangkan menambah fungsi khusus kepada kerangka kerja, memberi tumpuan kepada pemahaman seni bina, mengenal pasti titik lanjutan, dan amalan terbaik untuk integrasi dan debugging.

    See all articles