(string) 'hard-copy' 是如何复制字符串?

29

PHP使用的是复制-修改系统。

$a = (string) $a;($a已经是字符串)会修改和复制任何内容吗?


特别是,这是我的问题:

参数1是mixed/我想允许传递非字符串并将它们转换为字符串。
但有时这些字符串很大。所以我想省略已经是字符串的参数的复制。

我可以使用版本Foo还是必须使用版本Bar

class Foo {
    private $_foo;
    public function __construct($foo) {
        $this->_foo = (string) $foo;
    }
}

class Bar {
    private $_bar;
    public function __construct($bar) {
        if (is_string($bar)) {
            $this->_bar = $bar;
        } else {
            $this->_bar = (string) $bar;
        }
    }
}
3个回答

45
答案是:是的,它确实复制了字符串。有点儿...其实不是。好吧,这取决于你对“复制”的定义... >=5.4
为了了解正在发生什么,让我们看看源代码。执行器处理一个变量转换在这里使用5.5
    zend_make_printable_zval(expr, &var_copy, &use_copy);
    if (use_copy) {
        ZVAL_COPY_VALUE(result, &var_copy);
        // if optimized out
    } else {
        ZVAL_COPY_VALUE(result, expr);
        // if optimized out
        zendi_zval_copy_ctor(*result);
    }

正如您所看到的,该调用使用zend_make_printable_zval(),如果zval已经是字符串,则会直接短路处理。

因此,执行复制操作的代码是(else分支):

ZVAL_COPY_VALUE(result, expr);

现在,让我们来看一下ZVAL_COPY_VALUE的定义
#define ZVAL_COPY_VALUE(z, v)                   \
    do {                                        \
        (z)->value = (v)->value;                \
        Z_TYPE_P(z) = Z_TYPE_P(v);              \
    } while (0)

请注意这段代码的作用。字符串本身并没有被复制(它存储在zval的->value块中)。它只是被引用了(指针保持不变,因此字符串值相同,没有复制)。但它创建了一个新变量(包装值的zval部分)。
现在,我们进入zendi_zval_copy_ctor调用。它在内部执行一些有趣的操作。请注意:
case IS_STRING:
    CHECK_ZVAL_STRING_REL(zvalue);
    if (!IS_INTERNED(zvalue->value.str.val)) {
        zvalue->value.str.val = (char *) estrndup_rel(zvalue->value.str.val, zvalue->value.str.len);
    }
    break;

基本上,这意味着如果它是一个interned字符串,它不会被复制。但如果不是,它将会被复制...那什么是interned字符串,这是什么意思? <= 5.3
在5.3中,interned字符串不存在。所以字符串总是被复制。这就是唯一的区别...
基准测试时间:
好吧,在这种情况下:
$a = "foo";
$b = (string) $a;

在5.4中不会发生字符串的复制,但在5.3中会发生复制。
但是在这种情况下:
$a = str_repeat("a", 10);
$b = (string) $a;

所有版本都将会发生复制。这是因为在 PHP 中,不是所有字符串都被内部化了...

让我们在基准测试中试一下:http://3v4l.org/HEelW

$a = "foobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisoutfoobarbizbazbuztestingthisout";
$b = str_repeat("a", 300);

echo "Static Var\n";
testCopy($a);
echo "Dynamic Var\n";
testCopy($b);

function testCopy($var) {
    echo memory_get_usage() . "\n";
    $var = (string) $var;
    echo memory_get_usage() . "\n";
}

结果:

  • 5.4 - 5.5 alpha 1 (not including other alphas, as the differences are minor enough to not make a fundamental difference)

    Static Var
    220152
    220200
    Dynamic Var
    220152
    220520
    

    So the static var increased by 48 bytes, and the dynamic var increased by 368 bytes.

  • 5.3.11 to 5.3.22:

    Static Var
    624472
    625408
    Dynamic Var
    624472
    624840
    

    The static var increased by 936 bytes while dynamic var increased by 368 bytes.

请注意,在5.3中,静态变量和动态变量都被复制了。因此,字符串总是被复制的。
但是在5.4中,对于静态字符串,只有zval结构被复制。这意味着被内部化的字符串本身保持不变,不会被复制...
另外需要注意的是,上述所有内容都是无用的。您将变量作为参数传递给函数。然后您在函数内部进行转换。因此,写时复制将由您的代码行触发。因此,运行该代码将始终(在99.9%的情况下)触发变量复制。所以最好的情况(内部化字符串)是zval复制和相关开销。在最坏的情况下,您正在谈论字符串复制...

4
我喜欢了解事物的内在运作原理,而不仅仅是知道它是如何运作的。+1 - David J Eddy
我在所有四种情况下都得到了相同的内存使用量“347992”。运行的是PHP 7.0.0。这里有什么问题吗?(您的演示证实了这一点) - revo
1
@revo PHP7的变化有点大,因为字符串是一等实体,并且除非被修改,否则它们不会被复制。 - ircmaxell
@ircmaxell 抱歉,我需要一些澄清你所说的内容。我不熟悉一等实体,并且找不到一些好的相关信息。 - revo
1
@revo 是一个短语,意思是它们有专门的数据结构和管理系统。这意味着它们是一种定义好的类型,与变量分开管理(不像在5.x中,它与变量绑定)。 - ircmaxell
显示剩余2条评论

12

你的代码实际上并没有做到:

$a = (string)$a;

因为在将字符串作为函数参数传递时,采用了写时复制语义,所以更像是这样:

$b = (string)$a;

这两个声明之间存在相当大的差异。第一个不会有任何记忆影响,而第二个通常会有影响。

以下代码大致执行了您的代码所执行的操作;传递了某个字符串并将其转换并赋值给另一个变量。它会跟踪内存的增长。

<?php

$x = 0;
$y = 0;

$x = memory_get_usage();

$s = str_repeat('c', 1200);

$y = memory_get_usage();

echo $y - $x, PHP_EOL;

$s1 = (string)$s;

$x = memory_get_usage();

echo $x - $y, PHP_EOL;

结果(5.4.9)

1360
1360

结果 (5.3.19):

1368
1368

该赋值操作基本上是复制整个字符串的值。

使用字符串字面量

在使用字符串字面量时,行为取决于版本:

<?php

$x = 0;
$y = 0;

$x = memory_get_usage();

$s = 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc';

$y = memory_get_usage();

echo $y - $x, PHP_EOL;

$s1 = (string)$s;

$x = memory_get_usage();

echo $x - $y, PHP_EOL;

结果(5.4.9):

152
136

结果(5.3.19)

1328
1328

原因在于字符串字面量被引擎以不同的方式处理,您可以从ircmaxell的答案中了解到。


2
@KarolyHorvath 嗯,引用计数并不等同于内存消耗;我不确定这里是否与引用计数有关。 - Ja͢ck
这个答案是错误的,并且它的证明有一个漏洞.. 你重复使用了同一个变量名... 请看我的答案。 - Karoly Horvath
@KarolyHorvath 正如我之前所说,refcount !== memory consumption;创建一个新的符号来引用相同的值不算。 - Ja͢ck
@KarolyHorvath 的重点是,在您的测试脚本中,我们创建了一个新变量 $b = (string) $a。但问题在于 $a = (string) $a。由于没有新的变量且 $a 已经是一个字符串,因此 PHP 不会分配额外的内存。 - mzimmer
2
@MichelZimmer:但是如果你将$a传递到一个函数中,并在函数内部执行此操作,$a =(string)$a$b =(string)$a相同,因为它们都是基于写时复制语义的。所以是的,你应该关注第二个... - ircmaxell
显示剩余3条评论

7

令人惊讶的是,它确实创建了一个副本:

$string = "TestMe";
debug_zval_dump($string);

$string2 = $string;
debug_zval_dump($string);

$string3 = $string;
debug_zval_dump($string);

$string4 = (string) $string;
debug_zval_dump($string);

$string5 = (string) $string;
debug_zval_dump($string);

输出:

string(6) "TestMe" refcount(2)
string(6) "TestMe" refcount(3)
string(6) "TestMe" refcount(4)
string(6) "TestMe" refcount(4)
string(6) "TestMe" refcount(4)

另一个证明:
echo memory_get_usage(), PHP_EOL;

$s = str_repeat('c', 100000);
echo memory_get_usage(), PHP_EOL;

$s1 = $s;
echo memory_get_usage(), PHP_EOL;

$s2 = (string) $s;
echo memory_get_usage(), PHP_EOL;

输出:

627496
727664
727760  # small increase, new allocated object, but no string copy
827928  # oops, we copied the string...

如其他答案中所提到的,从5.3到5.4,内部发生了变化。现在运行第一段代码将会发出五次:_string(6) "TestMe" interned_。第二段代码也是如此,不会显示任何增加的内存使用量。 - lukas.j

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接