golang 和php float 计算结果不一致的坑

php > $f=(93.000-20.0000)/(140.0-20.0);
php > php > var_dump($f300.0,(93.0-20.0)/(140.0-20.0)300.0,$f);
float(182.5)
float(182.5)
float(0.60833333333333)



package main



import “fmt”



func main() {
f := (93.0 - 20.0) / (140.0 - 20.0)
fmt.Println(f,f300.0, (93.0-20.0)/(140.0-20.0)300.0)
}



0.6083333333333333 182.49999999999997 182.5
问题来了第二个结果不一样
会导致
func round(x float64){
return int(math.Floor(x + 0/5))
}
计算的结果和php round()函数不一致

package main



import “fmt”



func main() {
p := (float32(93) - float32(20)) / (float32(140) - float32(20))
fmt.Println((float64(93)-float64(20))/(float64(140)-float64(20))float64(300), (float32(93)-float32(20))/(float32(140)-float32(20))float32(300), p*float32(300))
}



182.49999999999997 182.5 182.5
可以发现,使用float32 两者结果一样了,回过头来看,php的float是float32,double 是float64



1 浮点数为什么不精确
先看两个case



// case1: 135.90100 ====
// float32
var f1 float32 = 135.90
fmt.Println(f1 * 100) // output:13589.999
// float64
var f2 float64 = 135.90
fmt.Println(f2 * 100) // output:13590
浮点数在单精度下, 135.9
100即出现了偏差, 双精度下结果正确.



// case2: 0.1 add 10 times ===
// float32
var f3 float32 = 0
for i := 0; i < 10; i++ {
f3 += 0.1
}
fmt.Println(f3) //output:1.0000001



// float64
var f4 float64 = 0
for i := 0; i < 10; i++ {
f4 += 0.1
}
fmt.Println(f4) //output:0.9999999999999999
0.1加10次, 这下无论是float32和float64都出现了偏差.



为什么呢, Go和大多数语言一样, 使用标准的IEEE754表示浮点数, 0.1使用二进制表示结果是一个无限循环数, 只能舍入后表示, 累加10次之后就会出现偏差.



此外, 还有几个隐藏的坑https://play.golang.org/p/bQPbirROmN



float32和float64直接互转会精度丢失, 四舍五入后错误.
int64转float64在数值很大的时候出现偏差.
合理但须注意: 两位小数乘100强转int, 比期望值少了1.
package main



import (
“fmt”
)



func main() {
// case: float32==>float64
// 从数据库中取出80.45, 历史代码用float32接收
var a float32 = 80.45
var b float64
// 有些函数只能接收float64, 只能强转
b = float64(a)
// 打印出值, 强转后出现偏差
fmt.Println(a) //output:80.45
fmt.Println(b) //output:80.44999694824219
// … 四舍五入保留小数点后1位, 期望80.5, 结果是80.4



// case: int64==>float64
var c int64 = 987654321098765432
fmt.Printf("%.f\n", float64(c)) //output:987654321098765440

// case: int(float64(xx.xx*100))
var d float64 = 1129.6
var e int64 = int64(d * 100)
fmt.Println(e) //output:112959 } ##2 数据库是怎么做的 MySQL提供了decimal(p,d)/numberlic(p,d)类型的定点数表示法, 由p位数字(不包括符号、小数点)组成, 小数点后面有d位数字, 占p+2个字节, 计算性能会比double/float类型弱一些.


##3 Go代码如何实现Decimal
Java有成熟的标准库java.lang.BigDecimal,Python有标准库Decimal, 可惜GO没有. 在GitHub搜decimal, star数量比较多的是TiDB里的MyDecimal和ithub.com/shopspring/decimal的实现.



shopspring的Decimal实现比较简单, 思路是使用十进制定点数表示法, 有多少位小数就小数点后移多少位, value保存移之后的整数, exp保存小数点后的数位个数, number=value*10^exp, 因为移小数点后的整数可能很大, 所以这里借用标准包里的math/big表示这个大整数. exp使用了int32, 所以这个包最多能表示小数点后有32个十进制数位的情况.



Decimal结构体的定义如下



// Decimal represents a fixed-point decimal. It is immutable.
// number = value * 10 ^ exp
type Decimal struct {
value *big.Int



// NOTE(vadim): this must be an int32, because we cast it to float64 during
// calculations. If exp is 64 bit, we might lose precision.
// If we cared about being able to represent every possible decimal, we
// could make exp a *big.Int but it would hurt performance and numbers
// like that are unrealistic.
exp int32 } TiDB里的MyDecimal定义位于github.com/pingcap/tidb/util/types/mydecimal.go, 实现比shopspring的Decimal复杂多了, 也更底层(不依赖math/big), 性能也更好(见下面的benchmark). 其思路是: digitsInt保存数字的整数部分数字个数, digitsFrac保存数字的小数部分数字个数, resultFrac保存计算及序列化时保留至小数点后几位, negative标明数字是否为负数, wordBuf是一个定长的int32数组(长度为9), 数字去掉小数点的主体保存在这里, 一个int32有32个bit, 最大值为(2**31-1)2147483647(10个十进制数), 所以一个int32最多能表示9个十进制数位, 因此wordBuf 最多能容纳9*9个十进制数位.


// MyDecimal represents a decimal value.
type MyDecimal struct {
digitsInt int8 // the number of decimal digits before the point.



digitsFrac int8 // the number of decimal digits after the point.

resultFrac int8 // result fraction digits.

negative bool

// wordBuf is an array of int32 words.
// A word is an int32 value can hold 9 digits.(0 <= word < wordBase)
wordBuf [maxWordBufLen]int32 } 看看这两种decimal类型在文首的两个case下的结果, 同时跑个分.


main_test.go



package main



import (
“testing”
“github.com/shopspring/decimal”
“github.com/pingcap/tidb/util/types”
“log”
)



var case1String = “135.90”
var case1Bytes = []byte(case1String)
var case2String = “0”
var case2Bytes = []byte(“0”)



func ShopspringDecimalCase1() decimal.Decimal {
dec1, err := decimal.NewFromString(case1String)
if err != nil {
log.Fatal(err)
}
dec2 := decimal.NewFromFloat(100)
dec3 := dec1.Mul(dec2)
return dec3
}



func TidbDecimalCase1() *types.MyDecimal {
dec1 := new(types.MyDecimal)
err := dec1.FromString(case1Bytes)
if err != nil {
log.Fatal(err)
}
dec2 := new(types.MyDecimal).FromInt(100)
dec3 := new(types.MyDecimal)
err = types.DecimalMul(dec1, dec2, dec3)
if err != nil {
log.Fatal(err)
}
return dec3
}



func ShopspringDecimalCase2() decimal.Decimal {
dec1, err := decimal.NewFromString(case2String)
if err != nil {
log.Fatal(err)
}
dec2 := decimal.NewFromFloat(0.1)
for i := 0; i < 10; i++ {
dec1 = dec1.Add(dec2)
}
return dec1
}



func TidbDecimalCase2() *types.MyDecimal {
dec1 := new(types.MyDecimal)
dec1.FromString(case2Bytes)
dec2 := new(types.MyDecimal)
dec2.FromFloat64(0.1)
for i := 0; i < 10; i++ {
types.DecimalAdd(dec1, dec2, dec1)
}
return dec1



}



// case1: 135.90*100 ====
func BenchmarkShopspringDecimalCase1(b *testing.B) {
for i := 0; i < b.N; i++ {
ShopspringDecimalCase1()
}
b.Log(ShopspringDecimalCase1()) // output: 13590
}



func BenchmarkTidbDecimalCase1(b *testing.B) {
for i := 0; i < b.N; i++ {
TidbDecimalCase1()
}
b.Log(TidbDecimalCase1()) // output: 13590.00
}



// case2: 0.1 add 10 times ===
func BenchmarkShopspringDecimalCase2(b *testing.B) {
for i := 0; i < b.N; i++ {
ShopspringDecimalCase2()
}
b.Log(ShopspringDecimalCase2()) // output: 1
}



func BenchmarkTidbDecimalCase2(b *testing.B) {
for i := 0; i < b.N; i++ {
TidbDecimalCase2()
}
b.Log(TidbDecimalCase2()) // output: 1.0
}
BenchmarkShopspringDecimalCase1-8 2000000 664 ns/op 340 B/op 10 allocs/op



BenchmarkTidbDecimalCase1-8 20000000 99.2 ns/op 48 B/op 1 allocs/op



BenchmarkShopspringDecimalCase2-8 300000 5210 ns/op 4294 B/op 111 allocs/op



BenchmarkTidbDecimalCase2-8 3000000 517 ns/op 83 B/op 3 allocs/op
可见两种实现在上面两个case下表示准确, TiDB的decimal实现的性能高于shopspring的实现, 堆内存分配次数也更少.



##4. MyDecimal的已知问题



用了一段时间后, tidb.MyDecimal也有一些问题



原版除法有bug, 可以通过除数和被除数同时放大一定倍数临时修复, 更好的解决方法需要官方人员解决, 已提issue, 这个bug真是匪夷所思. https://github.com/pingcap/tidb/issues/4873, 2017.11.3官方修复decimal除法问题:https://github.com/pingcap/tidb/pull/4995/files.
原版乘法有小问题, 行为不一致, 原版的from1和to不能为同一个指针, 但 Add Sub Div却可以. 可以通过copy参数修复.
移位小坑, 右移属于扩大数值, 没有问题. 左移有问题, 注意1左移两位不会变成0.01, 所以shift不要传负数.
round, 目前这个库的Round模式ModeHalfEven实际上是ModeHalfUp, 正常的四舍五入, 不是float的ModeHalfEven. 3.5=>4, 4.5=>5, 5.5=>6, 注意后期是否有变更.



php在使用加减乘除等运算符计算浮点数的时候,经常会出现意想不到的结果,特别是关于财务数据方面的计算,给不少工程师惹了很多的麻烦。比如今天工作终于到的一个案例:



$a = 2586;
$b = 2585.98;
var_dump($a-$b);
期望的结果是:float(0.02)
实际结果:



float(0.019999999999982)



二、防坑攻略:
1、通过乘100的方式转化为整数加减,然后在除以100转化回来……
2、使用number_format转化成字符串,然后在使用(float)强转回来……
3、php提供了高精度计算的函数库,实际上就是为了解决这个浮点数计算问题而生的。



三、为啥有坑:
php的bug?不是,这是所有语言基本上都会遇到的问题,所以基本上大部分语言都提供了精准计算的类库或函数库。



要搞明白这个原因, 首先我们要知道浮点数的表示(IEEE 754):



浮点数, 以64位的长度(双精度)为例, 会采用1位符号位(E), 11指数位(Q), 52位尾数(M)表示(一共64位).



符号位:最高位表示数据的正负,0表示正数,1表示负数。
指数位:表示数据以2为底的幂,指数采用偏移码表示
尾数:表示数据小数点后的有效数字.



这里的关键点就在于, 小数在二进制的表示, 小数如何转化为二进制呢?



算法是乘以2直到没有了小数为止。



0.9二进制表示为(从上往下): 1100100100100……



注意:上面的计算过程循环了,也就是说*2永远不可能消灭小数部分,这样算法将无限下去。很显然,小数的二进制表示有时是不可能精确的 。其实道理很简单,十进制系统中能不能准确表示出1/3呢?同样二进制系统也无法准确表示1/10。这也就解释了为什么浮点型减法出现了”减不尽”的精度丢失问题。



换句话说:我们看到十进制小数,在计算机内存储的不是一个精确的数字,也不可能精确。所以在数字加减乘除后出现意想不到的结果。



所以要比较两个浮点数,需要将其控制在我们需要的精度范围内再行比较,因此使用 bcadd() 函数来对浮点数想加并进行精度转换(为字符串):



var_dump(bcadd(0.2,0.7,1) == 0.9); // 输出:bool(true)



浮点数的存储精度跟系统有关,PHP 通常使用 IEEE 754 双精度格式,默认是20位有效精度。Floating point numbers



<?php
echo (int) ( (0.1+0.7) * 10 ); // 显示 7!



http://blog.100dos.com/2016/08/23/deep-understanding-of-float-type-in-PHP/



浮点数转换为整型
从浮点数转换成整数时,将向下取整。



向上取整:不管四舍五入的规则 只要后面有小数前面的整数就加1
向下取整:不管四舍五入的规则 只要后面有小数忽略小数



弱类型语言变量的实现



/* zend.h /
struct _zval_struct {
zvalue_value value; /
/
zend_uint refcount__gc;
zend_uchar type; /
活动类型 */
zend_uchar is_ref__gc;

}
PHP中的一个变量,zend虚拟机中,使用的是 _zval_struct 的结构体来描述,变量的值也是一个就结构体来描述.



_zval_struct的结构体是由 四个字段/域 (可以理解成关联数组)



zvalue_value value; /* 值 */



PHP变量的值,存储这个字段中。



具体存储的位置:



/* value 值 是一个 联合 /
/
zend.h /
typedef union _zval_value {
long lval; /
long value /
double dval; /
double value /
struct {
char * val;
int len;
} str;
HashTable *ht; /
hash table 指针 */
zend_object_value obj;
} zvalue_value;
Zend对变量的表示



zend实现了 zval结构体



{
value: [联合体] /* 联合体的内容可能是C语言中的long,double,hashtable(ht),obj, 联合体只能是其中一种类型,是一个枚举 */
type: 变量类型 , /
IS_NULL,IS_BOOL,IS_STRING, IS_LONG,IS_DOUBLE,IS_ARRAY,IS_OBJECT,IS_RESOURCE */
refcount_gc
is_ref_gc
}
C语言中类型对应PHP中的数据类型:



long -> int
double -> double
hashtable -> array
struct -> string
obj -> object
例如:



$a = 3;
{
value: [long lval = 3]
type: IS_LONG
}



$a = 3.5;
{
value: [double dval = 3.5]
type: IS_DOUBLE
}
变量类型的实现
zend_uchar type; /* 活动类型 */



可以根据上下文环境来强制转换。
例如:需要echo 的时候 就转换成 string
需要加减运算就 转换成 int



PHP 中有8中数据类型,为什么zval->value 联合体中,只有5中 ?
1: NULL,直接 zval->type = IS_NULL, 就可以表示,不必设置 value 的值。
2:BOOL, zval->type = IS_BOOL. 再设置 zval.value.lval = 1/0; (C语言中没有布尔值,都是通过1,0,来表示)
3: resource ,资源型,往往是服务器上打开一个接口,如果 文件读取接口。 zval->type = IS_RESOURCE, zval->type.lval = 服务器上打开的接口编号。



struct {
char * val;
int len;
} str;
PHP中,字符串类型,长度是已经缓存的,调用strlen时,系统可以直接返回其长度,不需要计算。



$b = ‘hello’;



/**




  • {

  • union_zvalue {

  • // 字符串的指针

  • struct{

  • char: ‘hello’;

  • len: 5

  • } str;

  • }

  • type: IS_STRING;

  • refcount_gc: 1,

  • is_ref_gc: 0

  • }

  • */



//在PHP中字符串的长度,是直接体现在其结构体中,所以调用strlen(); 速度非常快,时间复杂度为0(1)



echo strlen($b);



符号表
符号表symbol_table,变量的花名册



符号表是什么?



符号表示一张哈希表(哈希结构理解成关联数组)
里面存储了变量名-> 变量zval结构体的地址



struct _zend_executor_globals {


HashTable * active_symbol_table /* 活动符号表 /
HashTable symbol_table /
全局符号表 /
HashTable included_files; /
files already included */
}
// 变量花名册
$a = 3;
$b = 1.223;
$c = ‘hello’;



/**




  • 生成了3个结构体

  • 同时,全局符号表,中多了三条记录


  • a —> 0x123 —> 结构体 { 3 }

  • b —> 0x21a —> 结构体 { 1.223 }

  • c —> 0x1A0 —> 结构体 { hello }
    *
    */



// 变量声明
// 第一:结构体生成
// 第二:符号表中多了记录,变量的花名册
// 第三:指向结构体
传值赋值
传值赋值发生了什么



在传值赋值时:
以:b = $a;为例:
并没有再次产生结构体,而是2个变量共用1个结构体
此时,2个变量,指向同1个结构体
refcount_gc 值为 2 (如果没有指针指引,会有垃圾回收机制清除)



写时复制
cow写时复制特性



$a = 3;
$b = $a;



/**




  • 是否产生了2 个结构体?

  • 不是,共用1个, refcount_gc = 2;

  • */



$b = 5;



echo $a, $b; // 3, 5
// $a,$b 指向同一个结构体,那么,修改$b或$a,对方会不会受干扰 ? 没有干扰到对方。具有写时复制的特性
如果有一方修改,将会造成结构体的分裂



结构体一开始共用,到某一方要修改值时,才分裂。这种特性称为:COW 。Copy On Write。



引用赋值
引用赋值发生了什么



当引用赋值时,双方共用一个结构体(is_ref_gc=1)
强制分裂
<?php



// 强制分裂



$a = 3;
/**



  • {

  • value: 3;

  • type: IS_LONG;

  • refcount_gc: 1;

  • is_ref_gc: 0;

  • }
    */
    $b = $a;
    /**

  • {

  • value: 3;

  • type: IS_LONG;

  • refcount_gc: 2;

  • is_ref_gc: 0;

  • }
    */
    $c = &$a;
    // 不会按照 底下结构体变化
    /**

  • {

  • value: 3;

  • type: IS_LONG;

  • refcount_gc: 3;

  • is_ref_gc: 1;

  • }
    */



// 正确的结构体变化
// 如果is_ref_gc 0->1 的过程中(从0到1,表示想引用变量)。refcount_gc>1。多个变量共享一个变量值。将会产生强制分裂
/**



  • // $a $c 结构体

  • {

  • value: 3;

  • type: IS_LONG;

  • refcount_gc: 2;

  • is_ref_gc: 1;

  • }


  • // $b 结构体

  • {

  • value: 3;

  • type: IS_LONG;

  • refcount_gc: 1;

  • is_ref_gc: 0;

  • }

  • */



$c = 5;
// a c
/**



  • value: 5

  • type: IS_LONG;

  • refcount_gc: 2;

  • is_ref_gc: 1;
    */



// b
/**



  • value: 3

  • type: IS_LONG;

  • refcount_gc: 1;

  • is_ref_gc: 0;
    */



echo $a, $b, $c; // 5 , 3 , 5



引用数组时的一些奇怪现象



// 引用数组时的怪现象



$arr = array(0, 1, 2, 3);



$tmp = $arr;



$arr[1] = 11;



echo $tmp[1]; // 1



// 数组不会比较细致的检查,多维数组存在。 因此,判断的时候,只会判断外面 一层的 结构体。



数组不会比较细致的检查



// 先 引用 后 赋值
$arr = array(0, 1, 2, 3);



$x = &$arr[1];



$tmp = $arr;



$arr[1] = 999;



echo $tmp[1]; // 999 . hash表中的zvalue结构体中会变成引用类型。 // 只去关注外面一层结构体,而不去关注 hash表中的值。



echo ‘
’;



// 先赋值,后引用
$arr = array(0, 1, 2, 3);



$tmp = $arr;



$x = &$arr[1];



$arr[1] = 999;



echo $tmp[1]; // 1

循环数组
循环数组时的怪现象



// 循环数组时的怪现象
$arr = array(0, 1, 2, 3);



foreach ( $arr as $v ) {



}



var_dump(current($arr)); // 数组指针停留在数组结尾处, 取不到值. false



echo ‘
’;



$arr = array(0, 1, 2, 3);



foreach ( $arr as $val=>$key ) { // foreach 使用的 $arr 是 $arr的副本.
$arr[$key] = $val; // 修改之后,就会产生分裂。 foreach 遍历的是 $arr 的副本。 但是原数组的指针已经走了一步.
}



var_dump(current($arr)); // 1



$arr = array(‘a’, ‘b’, ‘c’, ‘d’);



foreach ( $arr as &$val ) { // 该foreach 会导致 $val = &$arr[3];



}



foreach ( $arr as $val ) {
print_r($arr);
echo ‘
’;
}
// 两个问题:
// 数组使用时,要慎用引用。
// foreach 使用后,不会把数组的内部指针重置, 使用数组时,不要假想内部指针指向数组头部. 也可以在foreach 之后 reset(); 指针。
符号表与作用域
当执行到函数时,会生成函数的“执行环境结构体”,包含函数名,参数,执行步骤,所在的类(如果是方法),以及为这个函数生成一个符号表。
符号表统一放在栈上,并把active_symbol_table指向刚产生的符号表。



// Zend/zend_compiles.h 文件中



// 源码:
struct _zend_execute_data {
struct _zend_op opline;
zend_function_state function_state;
zend_op_array *op_array;
zval *object;
HashTable *symbol_table;
struct _zend_execute_data *prev_execute_data;
zval *old_error_reporting;
zend_bool nested;
zval **original_return_value;
zend_class_entry *current_scope;
zend_class_entry *current_called_scope;
zval *current_this;
struct _zend_op *fast_ret; /
used by FAST_CALL/FAST_RET (finally keyword) */
zval *delayed_exception;
call_slot *call_slots;
call_slot *call;
};
// 简化:



struct _zend_execute_data {

zend_op_array *op_array; // 函数的执行步骤. 如果是函数调用。是函数调用的后的opcode
HashTable *symbol_table; // 此函数的符号表地址
zend_class_entry *current_scope; // 执行当前作用域
zval * current_this; // 对象 调用 this绑定
zval * current_object; // object 的指向

}



一个函数调用多次,会有多少个op_array ?
一个函数产生 一个
op_array. 调用多次,会产生多个 环境结构体, 会依次入栈,然后顺序执行。
调用多少次,就会入栈多少次。不同的执行环境,靠 唯一的 *op_array 来执行。



函数什么时候调用, 函数编译后的 opcode 什么时候执行。



$age = 23;



function t() {
$age = 3;
echo $age;
}



t();



/**



  • t 函数 在执行时,根据函数的参数,局部变量等,生成一个执行环境结构体。

  • 结构体 入栈,函数编译后的 opcode, 称为 op_array (就是执行逻辑)。开始执行, 以入栈的环境结构体为环境来执行。

  • 并生成此函数的 符号表, 函数寻找变量, 就在符号表中寻找。即局部变量。(一个环境结构体,就对应一张符号表)



  • 注意: 函数可能调用多次。栈中可能有某函数的多个执行环境 入栈。但是 op_array 只有一个。

  • */
    静态变量
    静态变量的实现



// Zend/zend_compile.h

struct _zend_op_array {
/* Common elements /
zend_uchar type;
const char *function_name;
zend_class_entry *scope;
zend_uint fn_flags;
union _zend_function *prototype;
zend_uint num_args;
zend_uint required_num_args;
zend_arg_info *arg_info;
/
END of common elements */



zend_uint *refcount;

zend_op *opcodes;
zend_uint last;

zend_compiled_variable *vars;
int last_var;

zend_uint T;

zend_uint nested_calls;
zend_uint used_stack;

zend_brk_cont_element *brk_cont_array;
int last_brk_cont;

zend_try_catch_element *try_catch_array;
int last_try_catch;
zend_bool has_finally_block;

/* static variables support */
HashTable *static_variables;

zend_uint this_var;

const char *filename;
zend_uint line_start;
zend_uint line_end;
const char *doc_comment;
zend_uint doc_comment_len;
zend_uint early_binding; /* the linked list of delayed declarations */

zend_literal *literals;
int last_literal;

void **run_time_cache;
int last_cache_slot;

void *reserved[ZEND_MAX_RESERVED_RESOURCES]; }; // 简化 struct _zend_op_array {
...
HashTable *static_variables; // 静态变量
... } 编译后的 op_array 只有一份。 静态变量并没有存储在符号表(symbol_table)中.而是存放在op_array中。


function t() {



static $age = 1;

return $age += 1;


}



echo t();
echo t();
echo t();



// 静态变量 不再和 执行的结构体, 也不再和 入栈的符号表有关。



常量



// Zend/zend_constants.h
// 常量结构体
typedef struct _zend_constant {
zval value; // 变量结构体
int flags; // 标志,是否大小写敏感等
char *name; // 常量名
uint name_len; //
int module_number; // 模块名
} zend_constant;
define函数的实现
define函数当然是 调用zend_register_constant声明的常量
具体如下:Zend/zend_builtin_functions.c



// 源码:



ZEND_FUNCTION(define)
{
char *name;
int name_len;
zval *val;
zval *val_free = NULL;
zend_bool non_cs = 0;
int case_sensitive = CONST_CS;
zend_constant c;



if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sz|b", &name, &name_len, &val, &non_cs) == FAILURE) {
return;
}

if(non_cs) {
case_sensitive = 0;
}

/* class constant, check if there is name and make sure class is valid & exists */
if (zend_memnstr(name, "::", sizeof("::") - 1, name + name_len)) {
zend_error(E_WARNING, "Class constants cannot be defined or redefined");
RETURN_FALSE;
}


repeat:
switch (Z_TYPE_P(val)) {
case IS_LONG:
case IS_DOUBLE:
case IS_STRING:
case IS_BOOL:
case IS_RESOURCE:
case IS_NULL:
break;
case IS_OBJECT:
if (!val_free) {
if (Z_OBJ_HT_P(val)->get) {
val_free = val = Z_OBJ_HT_P(val)->get(val TSRMLS_CC);
goto repeat;
} else if (Z_OBJ_HT_P(val)->cast_object) {
ALLOC_INIT_ZVAL(val_free);
if (Z_OBJ_HT_P(val)->cast_object(val, val_free, IS_STRING TSRMLS_CC) == SUCCESS) {
val = val_free;
break;
}
}
}
/* no break */
default:
zend_error(E_WARNING,”Constants may only evaluate to scalar values”);
if (val_free) {
zval_ptr_dtor(&val_free);
}
RETURN_FALSE;
}



c.value = *val;
zval_copy_ctor(&c.value);
if (val_free) {
zval_ptr_dtor(&val_free);
}
c.flags = case_sensitive; /* non persistent */
c.name = str_strndup(name, name_len);
if(c.name == NULL) {
RETURN_FALSE;
}
c.name_len = name_len+1;
c.module_number = PHP_USER_CONSTANT;
if (zend_register_constant(&c TSRMLS_CC) == SUCCESS) {
RETURN_TRUE;
} else {
RETURN_FALSE;
} } // 关键代码:


c.value = val;
zval_copy_ctor(&c.value);
if (val_free) {
zval_ptr_dtor(&val_free);
}
c.flags = case_sensitive; /
大小写敏感 /
c.name = str_strndup(name, name_len);
if(c.name == NULL) {
RETURN_FALSE;
}
c.name_len = name_len+1;
c.module_number = PHP_USER_CONSTANT; /
用户定义常量 */
if (zend_register_constant(&c TSRMLS_CC) == SUCCESS) {
RETURN_TRUE;
} else {
RETURN_FALSE;
}
常量就一个符号(哈希)表. 都使用一个符号表。所以全局有效。



常量的生成



int zend_register_constant(zend_constant c TSRMLS_DC) {


zend_hash_add(EG(zend_constants), name, c->name_len, (vaid
)c,sizeof(zend_constant, NULL) == FAILURE);


}
对象定义常量



class Dog {



public $name = 'kitty';

public function __toString () {
return $this->name;
}


}



$dog = new Dog();



define(‘DOG’, $dog);



print_r(DOG);



/**



  • define 值为对象时,会把对象装成标量来存储,需要类有 __toString魔术方法
    */



对象
对象的底层实现



Zend/zend.h



struct _zval_struct {
/* Variable information /
zvalue_value value; /
value /
zend_uint refcount__gc;
zend_uchar type; /
active type */
zend_uchar is_ref__gc;
};



// zvalue
typedef union _zvalue_value {
long lval; /* long value /
double dval; /
double value /
struct {
char *val;
int len;
} str;
HashTable *ht; /
hash table value */
zend_object_value obj;
zend_ast *ast;
} zvalue_value;



// 在 zend.h 中 查看到 zend_object_value obj; 是以zend_object_value 定义. 在Zend/zend_types.h 文件中继续查看



// Zend/zend_types.h
定义zend_object_value 结构体



typedef struct _zend_object_value {
zend_object_handle handle;
const zend_object_handlers *handlers;
} zend_object_value;
通过new出来的对象,返回的是什么。是zend_object_value. 并不是真正的对象,而是对象的指针。



返回的 handle再次指向对象。



每次new一个对象,对象就存入一张hash表中。(形象的称之为对象池)



对象存储时的特点:



// 对象



class Dog {
public $leg = 4;
public $wei = 20;
}



$dog = new Dog();



// $dog 是一个对象么?
// 严格说,并不是对象.
/**



  • {

  • handle –指向–> [hash表 {leg: 4, wei: 20}] // hash表中存在 对象

  • }
    */



$d2 = $dog;



$d2->leg = 5;



echo $dog->leg, ‘', $d2->leg; // 55



// 对象并不是 引用赋值. 主要原因 zval 结构体 是再次指向一个hash表中的 对象池
$d2 = false;



echo $dog->leg; // 5



内存分层
内存管理与垃圾回收



PHP封装了对系统内存的请求
不要直接使用malloc直接请求内存



PHP函数需要内存的时候,是通过emalloc,efree.
emalloc,efree向 mm_heap索要空间。



zend 中底层都离不开hash表。PHP中的HashTable太强大。



PHP 底层 所有的变量都是 放在 zend_mm_heap 中。 然后通过 各自的hash表来指向或跟踪。



zend虚拟机的运行原理



PHP语法实现



Zend/zend_language_scanner.l
Zend/zend_language_parser.y
OPcode编译



Zend/zend.compile.c
执行引擎



Zend/zend_vm_*
Zend/zend_execute.c


Category golang