php生成器详解 Generator以及yield到底是什么?

zh7314

2022年10月14日08:23:35

yield关键字
生成器函数的核心是yield关键字。它最简单的调用形式看起来像一个return申明,不同之处在于普通return会返回值并终止函数的执行,
而yield会返回一个值给循环调用此生成器的代码并且只是暂停执行生成器函数。
官方文档:https://www.php.net/manual/zh/language.generators.syntax.php

使用代码来看,yield返回的类型

function countTo4()
{
    yield from [1, 2, 3];
    yield 4;
}

$tt = countTo4();

print_r($tt);

PS D:\php_code\test_code> php .\1.php
Generator Object
(
)

Fatal error: The "yield" expression can only be used inside a function in D:\php_code\test_code\1.php on line 14
“yield”表达式只能在函数内部使用

Generator类的方法

final class Generator implements Iterator {
/* 方法 */
public current(): mixed
public getReturn(): mixed
public key(): mixed
public next(): void
public rewind(): void
public send(mixed $value): mixed
public throw(Throwable $exception): mixed
public valid(): bool
public __wakeup(): void
}

//测试方法
function countTo4()
{
    yield from [1, 2, 3];
    yield 4;
}
//获得生成器的返回
$data = countTo4();
//获取第一个元素
print_r($data->current());
//迭代器往下走一步
$data->next();
//迭代器往下走一步
$data->next();
//迭代器往下走一步
$data->next();
//注释行
//$data->next();
//获取最后一个元素
print_r($data->current());
//判断迭代器是否有效
print_r($data->valid());

如果打开注释行 //$data->next();迭代器就会失效,获取元素失败

注意:从php的源代码来看,yield只是标记方法为生成器,具体实现还是在zend vm里面处理

yield 异步,协程的关系

因为生成器是一个很好的接受返回数据的方法,不然很容易内存溢出,经常配合一些异步,协程执行获取返回结果,但是yield本身是和异步,协程并没有什么关系

生成器与 Iterator 对象的比较

生成器最主要的优点是简洁。和实现一个 Iterator 类相较而言, 同样的功能,用生成器可以编写更少的代码,可读性也更强。
https://www.php.net/manual/zh/language.generators.comparison.php

测试一下生成器的性能的例子,php版本是8.0 windows环境

echo '开始内存:' . (memory_get_usage() / 1024 / 1024) . 'M' . PHP_EOL;
function getDta()
{
    for ($i = 0; $i < 100000; $i++) {
        yield "data-$i";
    }
}
//            $rr = [];
//            for ($i = 0; $i < 100000; $i++) {
//                $rr[] = "data-$i";
//            }
echo '获取生成器内存:' . (memory_get_usage() / 1024 / 1024) . 'M' . PHP_EOL;
$data = getDta();
echo '结束内存:' . (memory_get_usage() / 1024 / 1024) . 'M' . PHP_EOL;

foreach ($data as $item) {
//                print_r($item . PHP_EOL);
}
echo '打印内存:' . (memory_get_usage() / 1024 / 1024) . 'M' . PHP_EOL;

在laravel执行结果是:

开始内存:4.0393676757812M
获取生成器内存:4.0393676757812M
结束内存:4.0397796630859M
打印内存:4.0397109985352M

纯php执行是:

PS D:\php_code\test_code> php .\1.php
开始内存:0.34362030029297M
获取生成器内存:0.34365081787109M
结束内存:0.34406280517578M
打印内存:0.343994140625M

laravel自身执行的文件按照约4m,但是生成器几乎没有占用内存
去掉注释

$rr = [];
for ($i = 0; $i < 100000; $i++) {
     $rr[] = "data-$i";
}

在laravel执行结果是:

开始内存:4.0398559570312M
获取生成器内存:13.853866577148M
结束内存:13.854278564453M
打印内存:13.854209899902M

纯php执行是:

PS D:\php_code\test_code> php .\1.php
开始内存:0.34410858154297M
获取生成器内存:10.158149719238M
结束内存:10.158561706543M
打印内存:10.158493041992M

内存消耗差别还是很大的,所以Generator具备优秀的减少内存消耗的特点,在很多高性能框架中使用

php源码对生成器和迭代器的追踪

这是zend引擎方法的名称
ZEND_YIELD
ZEND_YIELD_FROM

那些类型标记成生成器
static bool is_generator_compatible_class_type(zend_string *name) {
    return zend_string_equals_literal_ci(name, "Traversable")
        || zend_string_equals_literal_ci(name, "Iterator")
        || zend_string_equals_literal_ci(name, "Generator");
}

标记方法为生成器
static void zend_mark_function_as_generator(void) /* {{{ */
    {
        if (!CG(active_op_array)->function_name) {
            zend_error_noreturn(E_COMPILE_ERROR,
                "The \"yield\" expression can only be used inside a function");
        }

        if (CG(active_op_array)->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) {
            zend_type return_type = CG(active_op_array)->arg_info[-1].type;
            bool valid_type = (ZEND_TYPE_FULL_MASK(return_type) & MAY_BE_OBJECT) != 0;
            if (!valid_type) {
                zend_type *single_type;
                ZEND_TYPE_FOREACH(return_type, single_type) {
                    if (ZEND_TYPE_HAS_NAME(*single_type)
                            && is_generator_compatible_class_type(ZEND_TYPE_NAME(*single_type))) {
                        valid_type = 1;
                        break;
                    } 
                } ZEND_TYPE_FOREACH_END();
            }

            if (!valid_type) {
                zend_string *str = zend_type_to_string(return_type);
                zend_error_noreturn(E_COMPILE_ERROR,
                    "Generator return type must be a supertype of Generator, %s given",
                    ZSTR_VAL(str));
            }
        }

        CG(active_op_array)->fn_flags |= ZEND_ACC_GENERATOR;
    }

编译yield_from
    static void zend_compile_yield_from(znode *result, zend_ast *ast) /* {{{ */
        {
            zend_ast *expr_ast = ast->child[0];
            znode expr_node;

            zend_mark_function_as_generator();

            if (CG(active_op_array)->fn_flags & ZEND_ACC_RETURN_REFERENCE) {
                zend_error_noreturn(E_COMPILE_ERROR,
                    "Cannot use \"yield from\" inside a by-reference generator");
            }

            zend_compile_expr(&expr_node, expr_ast);
            zend_emit_op_tmp(result, ZEND_YIELD_FROM, &expr_node, NULL);
        }

        D:\src_code\20220824\php-src_1\Zend\zend_generators.c

创建一个生成器
static zend_object *zend_generator_create(zend_class_entry *class_type) /* {{{ */
    {
        zend_generator *generator = emalloc(sizeof(zend_generator));
        memset(generator, 0, sizeof(zend_generator));

        /* The key will be incremented on first use, so it'll start at 0 */
        generator->largest_used_integer_key = -1;

        ZVAL_UNDEF(&generator->retval);
        ZVAL_UNDEF(&generator->values);

        /* By default we have a tree of only one node */
        generator->node.parent = NULL;
        generator->node.children = 0;
        generator->node.ptr.root = NULL;

        zend_object_std_init(&generator->std, class_type);
        return (zend_object*)generator;
    }

yield_from转成生成器
    void zend_generator_yield_from(zend_generator *generator, zend_generator *from)
{
    ZEND_ASSERT(!generator->node.parent && "Already has parent?");
    zend_generator *leaf = clear_link_to_leaf(generator);
    if (leaf && !from->node.parent && !from->node.ptr.leaf) {
        from->node.ptr.leaf = leaf;
        leaf->node.ptr.root = from;
    }
    generator->node.parent = from;
    zend_generator_add_child(from, generator);
    generator->flags |= ZEND_GENERATOR_DO_INIT;
}

生成器的节点数据结构
struct _zend_generator_node {
    zend_generator *parent; /* NULL for root */
    uint32_t children;
    union {
        HashTable *ht; /* if multiple children */
        zend_generator *single; /* if one child */
    } child;
    /* One generator can cache a direct pointer to the current root.
     * The leaf member points back to the generator using the root cache. */
    union {
        zend_generator *leaf; /* if parent != NULL */
        zend_generator *root; /* if parent == NULL */
    } ptr;
};

zend_generator的数据结构体
struct _zend_generator {
    zend_object std;

    /* The suspended execution context. */
    zend_execute_data *execute_data;

    /* Frozen call stack for "yield" used in context of other calls */
    zend_execute_data *frozen_call_stack;

    /* Current value */
    zval value;
    /* Current key */
    zval key;
    /* Return value */
    zval retval;
    /* Variable to put sent value into */
    zval *send_target;
    /* Largest used integer key for auto-incrementing keys */
    zend_long largest_used_integer_key;

    /* Values specified by "yield from" to yield from this generator.
     * This is only used for arrays or non-generator Traversables.
     * This zval also uses the u2 structure in the same way as
     * by-value foreach. */
    zval values;

    /* Node of waiting generators when multiple "yield from" expressions
     * are nested. */
    zend_generator_node node;

    /* Fake execute_data for stacktraces */
    zend_execute_data execute_fake;

    /* ZEND_GENERATOR_* flags */
    zend_uchar flags;
};

生成器在代码上也是一个类似 parent -> children node的数据链表
但是在vm层实现了一种即使即用的模式,不在内存里面存储变量,减少php在处理大数据的时候出现内存溢出的问题。

php跨平台的实现也是zend vm带来的,和java的vm类似,使用虚拟机来屏蔽平台差异

zend vm实现生成器的原理还没完全弄明白,参考了
https://www.npopov.com/2017/04/14/PHP-7-Virtual-machine.html
中文翻译:https://www.jianshu.com/p/878cf85e07c5

Generators
生成器功能可能会暂停和恢复,因此需要特殊的VM堆栈管理。 这是一个简单的生成器:

function gen($x) {
    foo(yield $x);
}

这将产生以下opcodes:

$x = RECV 1
GENERATOR_CREATE
INIT_FCALL_BY_NAME (1 args) string("foo")
V1 = YIELD $x
SEND_VAR_NO_REF_EX V1 1
DO_FCALL_BY_NAME
GENERATOR_RETURN null

在到达GENERATOR_CREATE之前,这将在正常VM堆栈上作为正常函数执行。 然后,GENERATOR_CREATE创建一个对象,以及一个堆分配的execute_data结构(像往常一样,包括变量和参数的插槽),将vm堆栈上的execute_data复制到其中。生成器

当生成器再次恢复时,执行器将使用堆分配的execute_data,但将继续使用主VM堆栈推送调用帧。 这样做的一个明显问题是,在调用正在进行时可能会中断生成器,如前面的示例所示。 在这里,YIELD是在调用foo()的调用帧已经被推到VM堆栈上的点上执行的。

这种相对不常见的情况是通过在产生控制时将活动调用帧复制到生成器结构中,并在生成器恢复时恢复它们来处理的。

此设计自PHP7.1开始使用。 以前,每个生成器都有自己的4KIB VM内存页,当恢复生成器时,该页面将交换到执行器中。 这避免了复制调用帧的需要,但增加了内存使用。

zend_execute
EX
CG
EG

php debug

php –r 'print_r(Token_get_all("<?php echo \"hello world\";"));'

php -d vld.active=1 hello.php

# Opcache, since PHP 7.1
php -d opcache.opt_debug_level=0x10000 test.php

# phpdbg, since PHP 5.6
phpdbg -p* test.php

# vld, third-party extension
php -d vld.active=1 test.php

php代码的执行大概循序是:
php代码->词法解析语法解析->生成抽象语法树AST->AST编译成opcodes指令->通过zend虚拟机执行。
截图

606 1 0
1个评论

damao

这才是真大佬

  • 暂无评论

zh7314

690
积分
0
获赞数
0
粉丝数
2021-12-13 加入
🔝