PHP 内核分析:Zend 虚拟机

PHP 是一门解释型的语言。诸如 Java、Python、Ruby、Javascript
等解释型语言,我们编写的代码不会被编译成机器码运行,而是会被编译中间码运行在虚拟机(VM)上。运行
PHP 的虚拟机,称之为 Zend 虚拟机,今天我们将深入内核,探究 Zend
虚拟机运行的原理。

阅读指引读懂此文,需要以下基础:

简介

OPCODE

什么是 OPCODE?它是一种虚拟机能够识别并处理的指令。Zend
虚拟机包含了一系列的 OPCODE,通过 OPCODE 虚拟机能够做很多事情,列举几个
OPCODE 的例子:

  • ZEND_ADD 将两个操作数相加。
  • ZEND_NEW 创建一个 PHP 对象。
  • ZEND_ECHO 将内容输出到标准输出中。
  • ZEND_EXIT 退出 PHP。

诸如此类的操作,PHP 定义了186个(随着 PHP 的更新,肯定会支持更多种类的
OPCODE),所有的 OPCODE
的定义和实现都可以在源码的 zend/zend_vm_def.h 文件(这个文件的内容并不是原生的
C 代码,而是一个模板,后面会说明原因)中查阅到。

我们来看下 PHP 是如何设计 OPCODE 数据结构:

struct _zend_op {
    const void *handler;
    znode_op op1;
    znode_op op2;
    znode_op result;
    uint32_t extended_value;
    uint32_t lineno;
    zend_uchar opcode;
    zend_uchar op1_type;
    zend_uchar op2_type;
    zend_uchar result_type;
};

仔细观察 OPCODE 的数据结构,是不是能找到汇编语言的感觉。每一个 OPCODE
都包含两个操作数,op1和 op2handler 指针则指向了执行该 OPCODE
操作的函数,函数处理后的结果,会被保存在 result 中。

我们举一个简单的例子:

<?php
$b = 1;
$a = $b + 2;

我们通过 vld 扩展看到,经过编译的后,上面的代码生成了 ZEND_ADD 指令的
OPCODE。

compiled vars:  !0 = $b, !1 = $a
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   ASSIGN                                                   !0, 1
   3     1        ADD                                              ~3      !0, 2
         2        ASSIGN                                                   !1, ~3
   8     3      > RETURN                                                   1

其中,第二行是 ZEND_ADD 指令的
OPCODE。我们看到,它接收2个操作数,op1 是变量 $bop2 是数字常量1,返回的结果存入了临时变量中。在 zend/zend_vm_def.h 文件中,我们可以找到
ZEND_ADD 指令对应的函数实现:

ZEND_VM_HANDLER(1, ZEND_ADD, CONST|TMPVAR|CV, CONST|TMPVAR|CV)
{
    USE_OPLINE
    zend_free_op free_op1, free_op2;
    zval *op1, *op2, *result;

    op1 = GET_OP1_ZVAL_PTR_UNDEF(BP_VAR_R);
    op2 = GET_OP2_ZVAL_PTR_UNDEF(BP_VAR_R);
    if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_LONG)) {
        if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_LONG)) {
            result = EX_VAR(opline->result.var);
            fast_long_add_function(result, op1, op2);
            ZEND_VM_NEXT_OPCODE();
        } else if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_DOUBLE)) {
            result = EX_VAR(opline->result.var);
            ZVAL_DOUBLE(result, ((double)Z_LVAL_P(op1)) + Z_DVAL_P(op2));
            ZEND_VM_NEXT_OPCODE();
        }
    } else if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_DOUBLE)) {

    ...
}

上面的代码并不是原生的 C 代码,而是一种模板。

为什么这样做?因为 PHP 是弱类型语言,而其实现的 C
则是强类型语言。弱类型语言支持自动类型匹配,而自动类型匹配的实现方式,就像上述代码一样,通过判断来处理不同类型的参数。试想一下,如果每一个
OPCODE
处理的时候都需要判断传入的参数类型,那么性能势必成为极大的问题(一次请求需要处理的
OPCODE 可能能达到成千上万个)。

哪有什么办法吗?我们发现在编译的时候,已经能够确定每个操作数的类型(可能是常量还是变量)。所以,PHP
真正执行时的 C
代码,不同类型操作数将分成不同的函数,供虚拟机直接调用。这部分代码放在了 zend/zend_vm_execute.h 中,展开后的文件相当大,而且我们注意到还有这样的代码:

if (IS_CONST == IS_CV) {

完全没有什么意义是吧?不过没有关系,C
的编译器会自动优化这样判断。大多数情况,我们希望了解某个 OPCODE
处理的逻辑,还是通过阅读模板文件 zend/zend_vm_def.h 比较容易。顺便说一下,根据模板生成
C 代码的程序就是用 PHP 实现的。

  1. 至少写过5000行的代码;
  2. 汇编基础(静态数据段,代码段,堆栈段)。

澳门新浦京手机版 ,1、当Zend
engine解释器完成对脚本代码的分析后,便将它们生成可以直接运行的中间代码,也称为操作码(Operate
Code,opcode),opcode是一个四元组,(opcode, op1, op2,
result),它们分别代表操作码,第一操作数,第二操作数,结果

执行过程

准确的来说,PHP
的执行分成了两大部分:编译和执行。这里我将不会详细展开编译的部分,而是把焦点放在执行的过程。

通过语法、词法分析等一系列的编译过程后,我们得到了一个名为 OPArray
的数据,其结构如下:

struct _zend_op_array {
    /* Common elements */
    zend_uchar type;
    zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
    uint32_t fn_flags;
    zend_string *function_name;
    zend_class_entry *scope;
    zend_function *prototype;
    uint32_t num_args;
    uint32_t required_num_args;
    zend_arg_info *arg_info;
    /* END of common elements */

    uint32_t *refcount;

    uint32_t last;
    zend_op *opcodes;

    int last_var;
    uint32_t T;
    zend_string **vars;

    int last_live_range;
    int last_try_catch;
    zend_live_range *live_range;
    zend_try_catch_element *try_catch_array;

    /* static variables support */
    HashTable *static_variables;

    zend_string *filename;
    uint32_t line_start;
    uint32_t line_end;
    zend_string *doc_comment;
    uint32_t early_binding; /* the linked list of delayed declarations */

    int last_literal;
    zval *literals;

    int  cache_size;
    void **run_time_cache;

    void *reserved[ZEND_MAX_RESERVED_RESOURCES];
};

内容超多对吧?简单的理解,其本质就是一个 OPCODE
数组外加执行过程中所需要的环境数据的集合。介绍几个相对来说比较重要的字段:

  • opcodes 存放 OPCODE 的数组。
  • filename 当前执行的脚本的文件名。
  • function_name 当前执行的方法名称。
  • static_variables 静态变量列表。
  • last_try_catch try_catch_array 当前上下文中,如果出现异常
    try-catch-finally 跳转所需的信息。
  • literals 所有诸如字符串 foo 或者数字23,这样的常量字面量集合。

为什么需要生成这样庞大的数据?因为编译时期生成的信息越多,执行时期所需要的时间就越少。

接下来,我们看下 PHP 是如何执行 OPCODE。OPCODE
的执行被放在一个大循环中,这个循环位于 zend/zend_vm_execute.h 中的 execute_ex 函数:

ZEND_API void execute_ex(zend_execute_data *ex)
{
    DCL_OPLINE

    zend_execute_data *execute_data = ex;

    LOAD_OPLINE();
    ZEND_VM_LOOP_INTERRUPT_CHECK();

    while (1) {
        if (UNEXPECTED((ret = ((opcode_handler_t)OPLINE->handler)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) != 0)) {
            if (EXPECTED(ret > 0)) {
                execute_data = EG(current_execute_data);
                ZEND_VM_LOOP_INTERRUPT_CHECK();
            } else {
                return;
            }
        }
    }

    zend_error_noreturn(E_CORE_ERROR, "Arrived at end of main loop which shouldn't happen");
}

这里,我去掉了一些环境变量判断分支,保留了运行的主流程。可以看到,在一个无限循环中,虚拟机会不断调用
OPCODE
指定的 handler 函数处理指令集,直到某次指令处理的结果 ret 小于0。注意到,在主流程中并没有移动
OPCODE
数组的当前指针,而是把这个过程放到指令执行的具体函数的结尾。所以,我们在大多数
OPCODE 的实现函数的末尾,都能看到调用这个宏:

ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

在之前那个简单例子中,我们看到 vld 打印出的执行 OPCODE
数组中,最后有一项指令为 ZEND_RETURN 的 OPCODE。但我们编写的 PHP
代码中并没有这样的语句。在编译时期,虚拟机会自动将这个指令加到 OPCODE
数组的结尾。ZEND_RETURN 指令对应的函数会返回
-1,判断执行的结果小于0时,就会退出循环,从而结束程序的运行。

有以下或者类似知识就更好了:

2、因为PHP是构建在Zend虚拟机(Zend
VM)之上的,所以PHP的opcode就是Zend虚拟机中的指令

方法调用

如果我们调用一个自定义的函数,虚拟机会如何处理呢?

<?php
function foo() {
    echo 'test';
}

foo();

我们通过 vld 查看生成的 OPCODE。出现了两个 OPCODE
指令执行栈,是因为我们自定义了一个 PHP
函数。在第一个执行栈上,调用自定义函数会执行两个 OPCODE
指令:INIT_FCALL 和 DO_FCALL

compiled vars:  none
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   NOP
   6     1        INIT_FCALL                                               'foo'
         2        DO_FCALL                                      0
         3      > RETURN                                                   1

compiled vars:  none
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   3     0  E >   ECHO                                                     'test'
   4     1      > RETURN                                                   null

其中,INIT_FCALL 准备了执行函数时所需要的上下文数据。DO_FCALL 负责执行函数。DO_FCALL 的处理函数根据不同的调用情况处理了大量逻辑,我摘取了其中执行用户定义的函数的逻辑部分:

ZEND_VM_HANDLER(60, ZEND_DO_FCALL, ANY, ANY, SPEC(RETVAL))
{
    USE_OPLINE
    zend_execute_data *call = EX(call);
    zend_function *fbc = call->func;
    zend_object *object;
    zval *ret;

    ...

    if (EXPECTED(fbc->type == ZEND_USER_FUNCTION)) {
        ret = NULL;
        if (RETURN_VALUE_USED(opline)) {
            ret = EX_VAR(opline->result.var);
            ZVAL_NULL(ret);
        }

        call->prev_execute_data = execute_data;
        i_init_func_execute_data(call, &fbc->op_array, ret);

        if (EXPECTED(zend_execute_ex == execute_ex)) {
            ZEND_VM_ENTER();
        } else {
            ZEND_ADD_CALL_FLAG(call, ZEND_CALL_TOP);
            zend_execute_ex(call);
        }
    }

    ...

    ZEND_VM_SET_OPCODE(opline + 1);
    ZEND_VM_CONTINUE();
}

可以看到,DO_FCALL 首先将调用函数前的上下文数据保存到 call->prev_execute_data,然后调用 i_init_func_execute_data 函数,将自定义函数对象中的 op_array(每个自定义函数会在编译的时候生成对应的数据,其数据结构中包含了函数的
OPCODE 数组) 赋值给新的执行上下文对象。

然后,调用 zend_execute_ex 函数,开始执行自定义的函数。zend_execute_ex 实际上就是前面提到的 execute_ex 函数(默认是这样,但扩展可能重写 zend_execute_ex 指针,这个
API 让 PHP
扩展开发者可以通过覆写函数达到扩展功能的目的,不是本篇的主题,不准备深入探讨),只是上下文数据被替换成当前函数所在的上下文数据。

我们可以这样理解,最外层的代码就是一个默认存在的函数(类似 C
语言中的 main()函数),和用户自定义的函数本质上是没有区别的。

  1. C语言编译,C++对象模型,MFC反射的实现
  2. JAVA的解释器运行原理
  3. 使用过javascript,Python,PHP:感受过代码和类型系统在运行时的自由程度的不同
  4. 计算机组成原理

opcode结构

逻辑跳转

我们知道指令都是顺序执行的,而我们的程序,一般都包含不少的逻辑判断和循环,这部分又是如何通过
OPCODE 实现的呢?

<?php
$a = 10;
if ($a == 10) {
    echo 'success';
} else {
    echo 'failure';
}

我们还是通过 vld 查看 OPCODE(不得不说 vld 扩展是分析 PHP 的神器)。

compiled vars:  !0 = $a
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   ASSIGN                                                   !0, 10
   3     1        IS_EQUAL                                         ~2      !0, 10
         2      > JMPZ                                                     ~2, ->5
   4     3    >   ECHO                                                     'success'
         4      > JMP                                                      ->6
   6     5    >   ECHO                                                     'failure'
   7     6    > > RETURN                                                   1

我们看到,JMPZ 和 JMP 控制了执行流程。JMP 的逻辑非常简单,将当前的
OPCODE 指针指向需要跳转的 OPCODE。

ZEND_VM_HANDLER(42, ZEND_JMP, JMP_ADDR, ANY)
{
    USE_OPLINE

    ZEND_VM_SET_OPCODE(OP_JMP_ADDR(opline, opline->op1));
    ZEND_VM_CONTINUE();
}

JMPZ 仅仅是多了一次判断,根据结果选择是否跳转,这里就不再重复列举了。而处理循环的方式与判断基本上是类似的。

<?php
$a = [1, 2, 3];
foreach ($a as $n) {
    echo $n;
}

compiled vars:  !0 = $a, !1 = $n
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   ASSIGN                                                   !0, <array>
   3     1      > FE_RESET_R                                       $3      !0, ->5
         2    > > FE_FETCH_R                                               $3, !1, ->5
   4     3    >   ECHO                                                     !1
         4      > JMP                                                      ->2
         5    >   FE_FREE                                                  $3
   5     6      > RETURN                                                   1

循环只需要 JMP 指令即可完成,通过 FE_FETCH_R 指令判断是否已经到达数组的结尾,如果到达则退出循环。

序 —— 一些问题

struct _zend_op {
        opcode_handler_t handler;
        znode_op op1;
        znode_op op2;
        znode_op result;
        ulong extended_value;
        uint lineno;
        zend_uchar opcode;
        zend_uchar op1_type;
        zend_uchar op2_type;
        zend_uchar result_type;
};

结语

通过了解 Zend 虚拟机,相信你对 PHP
是如何运行的,会有更深刻的理解。想到我们写的一行行代码,最后机器执行的时候会变成数不胜数的指令,每个指令又建立在复杂的处理逻辑之上。那些从前随意写下的代码,现在会不会在脑海里不自觉的转换成
OPCODE 再品味一番呢?

  1. 程序设计语言的目的是什么?
  2. 为什么大多数语言有控制流?逐行执行+跳转。这与我们的需求差很远(例如一个教务管理系统、一个自动打车APP)
  3. 为什么类型申明在C语言中要与控制流隔离开来?
  4. 现在主流语言最基本的元素是?
  5. 有没有语言它的类型结构,在运行时也可以改变?动态性?

1、 opcode_handler_t  opcode的函数指针 参考地址 opcode
handler

什么是动态性?

2、result

  1. 编译后确定了什么信息,之后不再改变;
  2. 运行时可以改变、添加什么;
  3. 运行时是否保存着类型信息。

我们看一下两个输出函数 ,echo 和 print

程序中的信息分为几类?

void zend_do_print(znode *result, const znode *arg TSRMLS_DC) /* {{{ */
{       
        zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);

        opline->result_type = IS_TMP_VAR;
        opline->result.var = get_temporary_variable(CG(active_op_array));
        opline->opcode = ZEND_PRINT;
        SET_NODE(opline->op1, arg);
        SET_UNUSED(opline->op2);
        GET_NODE(result, opline->result);
}
/* }}} */

void zend_do_echo(const znode *arg TSRMLS_DC) /* {{{ */
{
        zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);

        opline->opcode = ZEND_ECHO;
        SET_NODE(opline->op1, arg);
        SET_UNUSED(opline->op2);
}
/* }}} */
  1. 数据信息a) 编译时Meta-Data元数据(类型框架、空间占用)b)
    运行时Meta-Data元数据(继承体系、用于new或者反射)(特别区别编译与运行的Meta-Data的不同。)c)
    堆栈段中地址偏移(C++的switch case中不能声明变量、共享内存)d)
    静态段中地址
  2. 指令信息a)
    代码段(动态性需要操作系统或者虚拟机支持,例如动态链接库,动态类加载,lisp语言自生成代码)

 我们可以看到print有result的设置
op2均为使用,从这里我们也能看出print和echo的区别来,print有返回值,而echo没有,这里的没有和返回null是不同的,
如果尝试将echo的值赋值给某个变量或者传递给函数都会出现语法错误

语言举例

3、op1,op2 记录参数

汇编语言

4、lineno 对应的行号

汇编语言没有动态性吗?没有。首先,寄存器、数据段、堆栈、代码段完全由程序员控制。完完全全是写死了的。然后,根据冯诺伊曼机的规则;取指令,执行,取指令,执行……

5、opcode 对应相应的操作

既然都有数据段了,还要堆栈段来做什么?这不是多余?一开始本没有堆栈,直到60年代出现了module模块化,才有了堆栈。汇编中的模块叫子程序,不过仍旧靠程序员全权控制。

6、extended_value  
和CPU的指令类似,有一个标示指令的opcode字段,以及这个opcode所操作的操作数,PHP不像汇编那么底层,
在脚本实际执行的时候可能还需要其他更多的信息,extended_value字段就保存了这类信息

堆栈和模块化的优点有?

 

  1. 递归
  2. 功能分离到模块,可复用
  3. 封装作用域

关于result op1 op2的结构znode

堆栈和模块化的缺点有?

typedef union _znode_op {
        zend_uint      constant;
        zend_uint      var;
        zend_uint      num;
        zend_ulong     hash;
        zend_uint      opline_num; /*  Needs to be signed */
        zend_op       *jmp_addr;
        zval          *zv;
        zend_literal  *literal;
        void          *ptr;        /* Used for passing pointers from the compile to execution phase, currently used for traits */
} znode_op;

typedef struct _znode { /* used only during compilation */
        int op_type;
        union {
                znode_op op;
                zval constant; /* replaced by literal/zv */
                zend_op_array *op_array;
                zend_ast *ast;
        } u;
        zend_uint EA;      /* extended attributes */
} znode;
  1. 时间上:保存现场、还原现场的代价(另,高级语言编译“消除尾递归”节约部分成本)
  2. 空间上:爆栈的危险

 op_type

C语言

#define IS_CONST    (1<<0)  
#define IS_TMP_VAR  (1<<1)  
#define IS_VAR      (1<<2)  
#define IS_UNUSED   (1<<3)    /* Unused variable */  
#define IS_CV       (1<<4)    /* Compiled variable */

C语言比起汇编多了什么东西?

IS_CONST:表示常量,例如$a = 1; $b =
“hello”;这些代码生成OP后,1和”hello”都是以常量类型操作数存在。
IS_TMP_VAR:表示临时变量,临时变量一般在前面加~来表示,这是一些OP执行过程中需要用到的中间变量,例如初始化一个数组的时候,就需要一个临时变量来暂时存储数组zval,然后将数组赋值给变量。
IS_VAR: 一般意义上的变量,以$开发表示
IS_UNUSED : 暂时不介绍,从名字来看应该是标识为不使用
IS_CV:这种类型的操作数比较重要,此类型是在PHP后来的版本中(大概5.1)中才出现,CV的意思是compiled
variable,即编译后的变量,变量都是保存在一个符号表中,这个符号表是一个哈希表,试想如果每次读写变量的时候都需要到哈希表中去检索,势必会对效率有一定的影响,因此在执行上下文环境中,会将一些编译期间生成的变量缓存起来,此过程以后再详细介绍。此类型操作数一般以!开头表示,比如变量$a=123;$b=”hello”这段代码,$a和$b对应的操作数可能就是!0和!1,
0和1相当于一个索引号,通过索引号从缓存中取得相应的值。

  1. 编译器
  2. 表达式(相比汇编,可以处理多个操作数了。)
  3. 函数与模块{}(真·模块化,栈操作无需程序员完成)
  4. 类型(原子类型、结构类型、数组、指针)
  5. 头文件,库总之,C语言并没有比汇编多了新的特性,它只是把汇编的繁琐操作抽象出来,让编译器完成,减轻程序员负担。

u

编译器的作用是?

此字段为一个联合体,根据op_type的不同,u取不同的值。比如op_type=IS_CONST的时候,u中的constant保存的就是操作数对
应的zval结构。例如$a=123时,123这个操作数中,u中的constant是一个IS_LONG类型的zval,其值lval为123

  1. 解析表达式,控制流(汇编中指令只有1-3个操作数,而表达式可以多个操作数)
  2. 模块和函数的抽象(完成堆栈中保存恢复现场的工作)
  3. 类型变量的管理(所有变量被替换成直接访问的地址,最快的访问速率)
  4. 代码优化

 

变量是替换成可以直接访问地址的?a)
编译时的Meta-Data(struct的成员,数组的长度,以便替换到指令流;只在编译器中维护,编译结束后丢弃)b)
计算出每个变量相对于该模块的偏移(一旦算出该偏移地址,将固定在执行码中,无法改变;就是说编译完成后,所有变量的偏移地址都固定下来了。)c)
对变量的存储进行管理(所有的变量/内存地址的布置,都是在编译时确定的;也就是说,可执行码中没有类型信息,只有地址,任何数据都是地址来操作,完全和汇编类似。至于寄存器的安排,那是更下一层的类似缓存策略算法的结果。)

编译出来的执行码与汇编的执行码有特征的区别吗?没有。特别是在编译器优化之后。无法通过执行码,区分汇编和C程序。打个比方,一只“程序猫”在黑笼子里,在里面喵喵的叫,无法通过它的叫声来判断它是“汇编猫”还是“C语言猫”。

从效率上来讲,C的多余代价在哪里?

  1. 编译的时间
  2. 模块的堆栈操作总之,经过优化的C程序执行码与汇编效率几乎相同。因为从理论上来说,C并没有引入运行时的新机制。我理解的C语言只是一种汇编的宏而已。

C++语言(推荐《深度探索C++对象模型》)

C++语言比C语言多了什么?

  1. 成员函数
  2. 类型继承体系
  3. 虚函数、虚继承
  4. 模板
  5. 涉及到了多种编程范式(开始更抽象,语言逐渐开始脱离冯氏结构。)其中,面向对象的思想,让程序与现实事物的关系更加紧密。程序设计的负担,也因为OO与设计模式的流行,而变得轻松。

编程范式是什么?就是一套指导思想行为准则。(例如,C是过程式,Haskell是函数式,JAVA是面向对象,Python是简单的大杂烩,shell是调用命令的,lua是调用c程序的,PHP是写页面的,ProLog是线性逻辑推理的。再例如,UML是描述规格specification的,XML是存储数据的。再再例如,CSS是描述网页表现的,HTML是描述网页内容的。javascript比较神奇,不敢说。)

C++有什么编程范式?

  1. 过程式(使用STL的类C语言编程)
  2. ADT式(自定义抽象数据类型,继承;但是不用new,不用virtual;拷贝构造;为了防止资源泄漏,也发明了RAII的方式进行资源的初始化和释放)
  3. 面向对象式(使用new,使用virtual,需要指针或引用;实现多态。)
  4. 泛型编程(《Modern C++ Design》各种奇淫技巧,业务层代码比较少遇到)

C++编译器是怎样实现的?C++开始有一个叫做cfront的编译器,即把C++语言先翻译成C语言。然后再用C编译器来编译,C的编译器并不知道此段代码是来自C++还是C。

C++语言特性分别是怎样实现?简单说。

  1. 成员变量:和C语言的struct
    类似,最后也会被直接替换成地址,便于高效访问。
  2. 成员函数:使用特殊函数名编码方案,翻译成C函数,并添加this指针作参数。(如___clsA12345func001(…,clsA
    *this))
  3. 类型继承体系:通过C++编译时的Meta-Data来实现。即在编译时,编译器是知道类型信息与继承体系的,但是编译成C语言后就丧失了此类型信息。
  4. 虚函数、虚继承:为了支持多态,这也是“面向对象”最重要的特性,使用了虚函数表和虚基类表。注意,运行时多态是通过运行时查表实现的。稍后详细说。
  5. 模板:通过代码复制的方式实现。每次编译都需要重新编译,不能编成库文件直接使用。

C++编译器的准则与virtual机制?

  1. 首先,C++的编译准则,希望做到与C一样的效率。希望做到以下:——a)
    没有运行时调用间接性。任何数据在运行时都是一个地址直接就访问到。——b)
    没有运行时的Meta-Data。无需通过Meta-Data来访问某个复杂的类层次。——c)
    所有的数据都希望用C中struct来实现,即在编译时就确定好对象及其成员地址。
  2. 以上,在过程式范式,与ADT范式中都是成立的。
  3. 但是,在面向对象范式中,渴望做到:需要维系着同一个继承体系成员结构的一致性,只有这样,才能保证运行时的多态性。即希望通过同一个入口,访问到父类或者子类的相同数据成员、函数成员,而不在乎具体对象的是父类还是子类。

C++的virtual机制如何实现的?a) 虚函数i.
虚函数,运行时,每个有虚函数的类型都维持着一个虚函数表,这已经是运行时的Meta-Data,通过查表,即可找到对象自己的虚函数。ii.
例如下图中clone肯定是object.__vptr__Base->#3(),无论具体的对象。b)
多重继承——如何处理后继的base基类?由编译器判断指针类型并加上相应的偏移。c)
虚继承——添加一个虚基类指针,指向共享部分。这样的缺点有两个:

  1. 虚基类的子类都要背负一个基类指针指向共享部分。如果继承了多个虚基类,还需要多个这样的指针。(Microsoft的解决方法是增加一个虚基类表,类似于虚函数表。)
  2. 虚继承链条的增加,会导致间接访问的层次增加。例如两个菱形继承的串联。

跨平台的级别有哪些级别?——头文件,库,源代码

  1. C++确实在源代码的层次是可能跨平台的(例如《POSA2》中加了针对不同平台的各种宏的代码)。
  2. 也可以通过相同的头文件去访问不同平台的库。
  3. 但是,不同操作系统中的不同的API大大增加了跨平台的难度。
  4. 跨平台的责任留给了程序员(充斥着大量宏的C++跨平台代码确实让人头疼。)
  5. 编译器面对不同的系统也不敢作为,它只是负责编译源代码,链接。

如何使用C++才能保证其高效性能?

  1. 有额外负担的机制:虚函数,虚继承,拷贝构造。
  2. 用一次虚函数,多了一次指针寻址的效率损失,并且相对于inline内联(另,inline是编译器优化的重头),还损失了保存和恢复现场的效率。
  3. 用一次虚继承,也多一次指针寻址的效率损失。(另,虚基类没有成员变量没有虚函数的时候会被优化。这也是JAVA可以多重继承接口interface的原因。)
  4. 不要使用virtual在复杂的多继承,深层次继承中。
  5. 编译速度会较慢:virtual机制会使编译器处理更多的Meta-Data。

JAVA语言(推荐《本地Java代码的静态编译和动态编译问题》)

JAVA语言比C++语言多了什么?

  1. 虚拟机a) 跨平台b) 动态编译c) 动态特性
  2. 没有指针
  3. 没有类的多继承,有接口的多继承。
  4. 统一的库从编译来说,JAVA比C++迈出了一大步。它的跨平台特性和运行时的灵活性,为JAVA自己以及未来语言都提供了很多可能性。

虚拟机的好处有什么?

  1. 跨平台:在OS与字节码间隔了一层。实现了程序员无负担的跨平台。
  2. 动态编译:许多信息不必在编译后确定,为动态特性提供可能,稍后详细说。
  3. 运行时维护着类型信息,甚至可以加载新的类型。(CORBRA依赖这个实现)

JAVA编译执行的过程是怎样的?

  1. 编译后产生一个基于堆栈的字节码。
  2. JRE在不同的OS上提供支持。
  3. 起初的JRE是解释执行的,效率低下。a) 获取待执行的下一个字节码。b)
    解码。c) 从操作数堆栈获取所需的操作数。d) 按照 JVM 规范执行操作。e)
    将结果写回堆栈。

JAVA是如何解决执行效率低下的问题呢?使用JIT(Just-in-time)编译器进行动态编译。

JIT(Just-in-time)是怎样运行的呢?如何解决了效率的问题?如上图:

  1. 每次按照一个function来编译。转成中间表示,并优化其效率,再生成可执行码。
  2. 编译器的编译线程和执行线程是分开的,应用程序不会等待编译的执行。
  3. 分析框架Profiler会观察程序行为,对频繁执行的function进一步优化。(例如function内部对象维持一个池不必每次生成。)

动态编译的优点有什么?可以根据程序的行为,优化其代码

  1. 例如频繁执行的function——热方法
  2. 例如arrayCopy方法,如果每次都拷贝大段内存,在指令集中有特别指令可以加速。
  3. 例如类层次结构,多态的优化。(大多数虚调用都有其固定的一个目标,JIT因此生成的直接调用代码比虚表调用代码的效率会更高。)

动态编译的缺点有什么?

  1. 大量的初始编译会影响程序启动时间。
  2. 运行时候的编译,行为分析都需要花费时间。
  3. 运行效率达到稳定需要时间。
  4. 实时GUI型的程序不能忍受“动态编译”和“GC”带来的延迟。JAVA如何解决实时的需求?

使用AOT(Ahead-of-time)编译器:预先编译成为可执行码。AOT(Ahead-of-time)的缺点:对于一些动态特性的支持效率低下

  1. 反射机制
  2. 运行时类加载JIT与AOT的对比

总体来说,JAVA适合怎样的应用呢?JAVA比较时候需要长期运行的应用,例如Web服务器,Daemon服务。

函数式语言

函数式语言通常有哪些呢?

  1. 函数式语言a) Lispb) Schemec) Haskell F#?
  2. 包含了函数式特性的语言a) Pythonb) Javascriptc) JAVAd) C?

函数式语言有哪些特性?

  1. 函数无副作用,只对输入输出有作用
  2. 高阶函数,lamda演算。(这个像C函数指针,但是它是高阶的,即返回值可能也是函数)
  3. 没有过程,类似规格说明的语法,更容易理解,自解释。
  4. 基于list的编程,函数更通用。
  5. 惰性计算(这个很像“树形DP”)
  6. 有对应的数学形式化表达,有可能证明其正确性。(最终目标可能是保证程序没有bug。)
  7. 其模型适合多核或者分布式的计算。a) 不变性(immutable)b)
    惰性计算/按需计算(lazy evaluation)c)
    最重要的是,由于函数式语言不可在同一数据上做修改,每一次运用一个函数都会在新的位置产生新的数据,这与过程式语言在同一位置对数据做多次操作不同:函数式语言的函数依赖于前一次函数产生的结果数据,过程式语言依赖于数据的位置。这里函数式语言就暗含了计算的依赖顺序,如果没有前后顺序关系,就可以并发。而过程式语言没有指定这个顺序,就需要通过加锁、Actor、Channel等模式来指定这个顺序总的来说,函数式语言,向着更抽象迈了一大步,更像是数学上的表达,几乎与冯诺伊曼体系断绝了关系。

函数式语言的劣势?

  1. 效率不高(因为其抽象,远离了冯诺伊曼体系)
  2. 平台以及开发环境都比较简单。
  3. 缺少推广,应用不广泛

逻辑程序设计ProLog语言,线性逻辑。人工智能语言。没有接触过。

总结动态性有哪些呢?

  1. 多态性:运行时根据具体对象来访问属于它的方法。(而不理会指针的类型。)
  2. 反射:运行时维系着类型结构的Meta-Data。
  3. 运行时类加载:运行后再次加载新的数据类型和指令流。
  4. 动态链接:OS根据按需链接库文件。

编译语言 和 解释语言
的分界在哪里?语言本身并没有编译类型或者解释类型。(例如:JAVA也可以静态编译后成可执行码。)只有少数运行时特性是依赖于解释型的。(可能需要运行环境的支持。)

为什么解释语言都需要虚拟机或者运行环境支持?动态编译,运行时Meta-Data的保存,这些功能对于每个程序都是一致的。所以把它们分离开来,不必每个程序植入这些代码

非脚本语言 和
脚本语言脚本语言,我理解是负责调度其他代码的语言。例如shell脚本,lua。

跨平台分为哪些层次?

  1. 源码跨平台(C,C++,但是因为系统调用接口不同,程序员负担太大,但是汇编却不是。)
  2. 执行码跨平台(JAVA,有些语言直接从源码解释执行,例如Javascript,PHP)

发展历史(推荐《近看图灵碗 (一. 从苏黎世到巴黎)》)

学术上有哪些实验性语言?

  1. Fortran
  2. ALGOL58
  3. ALGOL60
  4. Lisp
  5. smalltalk

常用语言过程式:C,ALGOL,Pascal,面向对象式:C++,smalltalk,JAVA,Delphi函数式:Lisp,Scheme,Haskell,逻辑式:Prolog脚本?PHP,Python,Ruby存储描述信息:XML,CSS,HTML

回答问题程序设计语言的目的是什么?

  1. 控制数据
  2. 控制指令流为什么大多数语言有控制流?逐行执行+跳转。这与我们的需求差很远(例如一个教务管理系统。)逐行执行,很大程度是起源于冯诺依曼体系结构。

为什么类型申明在C语言中要与控制流隔离开来?因为在编译时,具体的类型信息,要转化成地址偏移,然后替换控制流中的类型变量。

现在主流语言最基本的元素是?控制流 与 类型系统。

有没有语言它的类型结构,在运行时也可以改变?Javascript只有对象没有类,使用prototype的方式继承,运行时给某个对象添加新的数据成员。没有类型体系。许多后来的语言在运行时都保存着类型信息的,例如Python,JAVA。

综上所述控制流——指令流类型系统——为了计算出变量地址信息区分运行时的Meta-Data与编译时的Meta-Data

发表评论

电子邮件地址不会被公开。 必填项已用*标注