max/min 函数(PHP)的一个小 BUG
先直接来看一段展示:
# 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

핫 AI 도구

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

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

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

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

인기 기사

뜨거운 도구

메모장++7.3.1
사용하기 쉬운 무료 코드 편집기

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

스튜디오 13.0.1 보내기
강력한 PHP 통합 개발 환경

드림위버 CS6
시각적 웹 개발 도구

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

뜨거운 주제











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

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

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

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

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

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

Alipay PHP ...

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