목차
ceil 会返回 -0.0
不能直接传入 -0.0
直接调用或赋值给变量
最后的原因
后话
백엔드 개발 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
로그인 후 복사

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

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

>>> $a = ceil(-0.5)=> -0.0>>> max($a, 0)=> -0.0
로그인 후 복사

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

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));    }    ...}
로그인 후 복사

从这里可以看出来 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;}
    로그인 후 복사

    以上代码在我机器上的执行结果是 -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
    로그인 후 복사

    注意到需要为 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;}
    로그인 후 복사

    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; }
    로그인 후 복사

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

  • 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); }...;
    로그인 후 복사

    为了直观 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
    로그인 후 복사

    序号为 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;    ...}
    로그인 후 복사

    函数执行过程中使用 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];    }}
    로그인 후 복사

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

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

    # Psy Shell v0.3.3 (PHP 5.5.30 — cli) by Justin Hileman>>> max(0, ceil(-0.5))=> 0
    로그인 후 복사

    后话

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

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

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

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

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

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

    본 웹사이트의 성명
    본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.

    핫 AI 도구

    Undresser.AI Undress

    Undresser.AI Undress

    사실적인 누드 사진을 만들기 위한 AI 기반 앱

    AI Clothes Remover

    AI Clothes Remover

    사진에서 옷을 제거하는 온라인 AI 도구입니다.

    Undress AI Tool

    Undress AI Tool

    무료로 이미지를 벗다

    Clothoff.io

    Clothoff.io

    AI 옷 제거제

    AI Hentai Generator

    AI Hentai Generator

    AI Hentai를 무료로 생성하십시오.

    인기 기사

    R.E.P.O. 에너지 결정과 그들이하는 일 (노란색 크리스탈)
    3 몇 주 전 By 尊渡假赌尊渡假赌尊渡假赌
    R.E.P.O. 최고의 그래픽 설정
    3 몇 주 전 By 尊渡假赌尊渡假赌尊渡假赌
    R.E.P.O. 아무도들을 수없는 경우 오디오를 수정하는 방법
    3 몇 주 전 By 尊渡假赌尊渡假赌尊渡假赌

    뜨거운 도구

    메모장++7.3.1

    메모장++7.3.1

    사용하기 쉬운 무료 코드 편집기

    SublimeText3 중국어 버전

    SublimeText3 중국어 버전

    중국어 버전, 사용하기 매우 쉽습니다.

    스튜디오 13.0.1 보내기

    스튜디오 13.0.1 보내기

    강력한 PHP 통합 개발 환경

    드림위버 CS6

    드림위버 CS6

    시각적 웹 개발 도구

    SublimeText3 Mac 버전

    SublimeText3 Mac 버전

    신 수준의 코드 편집 소프트웨어(SublimeText3)

    PHP 로깅 : PHP 로그 분석을위한 모범 사례 PHP 로깅 : PHP 로그 분석을위한 모범 사례 Mar 10, 2025 pm 02:32 PM

    PHP 로깅은 웹 애플리케이션을 모니터링하고 디버깅하고 중요한 이벤트, 오류 및 런타임 동작을 캡처하는 데 필수적입니다. 시스템 성능에 대한 귀중한 통찰력을 제공하고 문제를 식별하며 더 빠른 문제 해결을 지원합니다.

    Laravel의 플래시 세션 데이터로 작업합니다 Laravel의 플래시 세션 데이터로 작업합니다 Mar 12, 2025 pm 05:08 PM

    Laravel은 직관적 인 플래시 방법을 사용하여 임시 세션 데이터 처리를 단순화합니다. 응용 프로그램에 간단한 메시지, 경고 또는 알림을 표시하는 데 적합합니다. 데이터는 기본적으로 후속 요청에만 지속됩니다. $ 요청-

    PHP의 컬 : REST API에서 PHP Curl Extension 사용 방법 PHP의 컬 : REST API에서 PHP Curl Extension 사용 방법 Mar 14, 2025 am 11:42 AM

    PHP 클라이언트 URL (CURL) 확장자는 개발자를위한 강력한 도구이며 원격 서버 및 REST API와의 원활한 상호 작용을 가능하게합니다. PHP CURL은 존경받는 다중 프로모토콜 파일 전송 라이브러리 인 Libcurl을 활용하여 효율적인 execu를 용이하게합니다.

    Laravel 테스트에서 단순화 된 HTTP 응답 조롱 Laravel 테스트에서 단순화 된 HTTP 응답 조롱 Mar 12, 2025 pm 05:09 PM

    Laravel은 간결한 HTTP 응답 시뮬레이션 구문을 제공하여 HTTP 상호 작용 테스트를 단순화합니다. 이 접근법은 테스트 시뮬레이션을보다 직관적으로 만들면서 코드 중복성을 크게 줄입니다. 기본 구현은 다양한 응답 유형 단축키를 제공합니다. Illuminate \ support \ Facades \ http를 사용하십시오. http :: 가짜 ([ 'google.com'=> ​​'Hello World', 'github.com'=> ​​[ 'foo'=> 'bar'], 'forge.laravel.com'=>

    Codecanyon에서 12 개의 최고의 PHP 채팅 스크립트 Codecanyon에서 12 개의 최고의 PHP 채팅 스크립트 Mar 13, 2025 pm 12:08 PM

    고객의 가장 긴급한 문제에 실시간 인스턴트 솔루션을 제공하고 싶습니까? 라이브 채팅을 통해 고객과 실시간 대화를 나누고 문제를 즉시 해결할 수 있습니다. 그것은 당신이 당신의 관습에 더 빠른 서비스를 제공 할 수 있도록합니다.

    PHP에서 늦은 정적 결합의 개념을 설명하십시오. PHP에서 늦은 정적 결합의 개념을 설명하십시오. Mar 21, 2025 pm 01:33 PM

    기사는 PHP 5.3에 도입 된 PHP의 LSB (Late STATIC BING)에 대해 논의하여 정적 방법의 런타임 해상도가보다 유연한 상속을 요구할 수있게한다. LSB의 실제 응용 프로그램 및 잠재적 성능

    프레임 워크 사용자 정의/확장 : 사용자 정의 기능을 추가하는 방법. 프레임 워크 사용자 정의/확장 : 사용자 정의 기능을 추가하는 방법. Mar 28, 2025 pm 05:12 PM

    이 기사에서는 프레임 워크에 사용자 정의 기능 추가, 아키텍처 이해, 확장 지점 식별 및 통합 및 디버깅을위한 모범 사례에 중점을 둡니다.

    See all articles