鍍金池/ 問答/人工智能  PHP/ PHP可變長參數(shù)(...)和生成器問題

PHP可變長參數(shù)(...)和生成器問題

問題來源于 https://segmentfault.com/q/10... 這里??戳?a href="/u/elarity">@elarity 的回答,他使用了200000的元素插入到redis集合。于是乎我使用了1百萬個元素數(shù)組來插入,在我這里是內(nèi)存溢出的,所以我使用了生成器的方式

function xrange() {
        for ($i=0; $i<1000000; $i++) {
                yield $i;
        }
}
$r = xrange();

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$key = 'jimu';
$redis->del($key);
$begin = microtime(true);
$redis->sadd($key, ...$r);

$end = microtime(true);
echo ($end - $begin) . "\n";

輸出結(jié)果:

[vagrant@localhost ~]$ php redis.php 
1.2786898612976
[vagrant@localhost ~]$

然后redis-cli中確實有了一百萬個元素。那么當我把代碼中的一百萬修改為一千萬的時候又報內(nèi)存溢出

[vagrant@localhost ~]$ php redis.php 
PHP Fatal error:  Allowed memory size of 134217728 bytes exhausted (tried to allocate 32 bytes) in /home/vagrant/redis.php on line 6

Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 32 bytes) in /home/vagrant/redis.php on line 6

根據(jù)我理解的生成器知識不應(yīng)該出現(xiàn)內(nèi)存溢出的情況。因為自始至終生成器xrange只占用一個變量($i)內(nèi)存?
所以我猜測是不是$redis->sadd($key, ...$r);這一步的時候...$r依然會解析成大數(shù)組。 現(xiàn)在不知道如何證實。


補充:
我在sadd之前使用var_dump(...$r);exit;發(fā)現(xiàn)輸出的都是

int(999775)
int(999776)
int(999777)
int(999778)
int(999779)
int(999780)
int(999781)

這樣可以證明生成器確實是一個一個產(chǎn)出值的。那么為什么將...$r傳入到sadd()的時候還報內(nèi)存不足呢?不明白這個...的原理,還請大佬們指點。

回答
編輯回答
孤星

好久沒看到想答的問題了,來一波

a. 這個問題和redis毫無關(guān)系

b. 上代碼

<?php
//splat.php
function gen() {
  global $argv;
  $max = $argv[1];
  while($max--) {
    yield(str_repeat('x', 10000));
  }
}

function noop() {

}

function getargs() {
  $arg = func_get_args();
}

function splat(...$arg) {

}

function printmemory($msg) {
  printf("%s: %d/%d\n", $msg, memory_get_usage(), memory_get_peak_usage());
}

printmemory(__LINE__);
$gen = gen();
printmemory(__LINE__);
foreach(gen() as $r) {
  crc32($r);
}
printmemory(__LINE__);
$argv[2](...$gen);
printmemory(__LINE__);
~/Desktop $ php splat.php 10000 getargs
27: 357896/394272
29: 358504/394272
33: 370816/394272
35: 382912/123779064
~/Desktop $ php splat.php 10000 noop
27: 357896/394272
29: 358504/394272
33: 370816/394272
35: 382912/123250912
~/Desktop $ php splat.php 10000 splat
27: 357896/394272
29: 358504/394272
33: 370816/394272
35: 382912/123779064
~/Desktop $ php splat.php 1000 splat
27: 357896/394272
29: 358504/394272
33: 370816/394272
35: 382912/12695544
~/Desktop $ php splat.php 100 splat
27: 357896/394272
29: 358504/394272
33: 370816/394272
35: 382912/1607672

c. 解釋

27-29-33之間,幾乎沒有內(nèi)存占用,這是所謂的"生成器節(jié)省內(nèi)存”的現(xiàn)象,也就是各種相關(guān)文章里都會解釋的,在30行迭代生成器的時候,每次循環(huán)都會進到生成器內(nèi)部去yield一次,產(chǎn)生一個大字符串,下次循環(huán)的時候循環(huán)變量又重新被賦值,之前的字符串自然會被GC回收,所以無論循環(huán)多大多少次,占用的內(nèi)存是穩(wěn)定的(包括上面的$gen=gen()也是幾乎不占內(nèi)存的)

33-35,無論被調(diào)用的函數(shù)如何,甚至noop函數(shù),都一樣會占用大量內(nèi)存,占用內(nèi)存的量明顯和次數(shù)成正比,也就是說生成器的內(nèi)容被合并到一起而占用了一整塊內(nèi)存。這其實很容易解釋,幾乎的所有語言“調(diào)用函數(shù)”的過程都是類似的

  1. 首先計算所有參數(shù),形成參數(shù)列表
  2. 生成call frame(其中包含調(diào)用被調(diào)雙方、文件行號、參數(shù)列表等等信息),壓入call stack中
  3. 控制權(quán)移交給函數(shù)內(nèi)部

(當然省略了超級多的細節(jié),比如實參形參的映射/copy啊,內(nèi)存管理啊等等什么的,和本題無關(guān))

...$args這個操作符其實影響的就是第一個階段,計算參數(shù)的時候,看到...操作符,就需要展開其中的參數(shù)來形成參數(shù)列表,那么用生成器的場合,這個階段內(nèi)存就從原有生成器的少量占用變成了完整的占用了,所以即使是空的noop函數(shù)也會占用幾乎一樣多的內(nèi)存,你的理解是正確的

回到原題的那個redis問題的話,因為重復(fù)調(diào)用redis方法一定會占用大量的額外網(wǎng)絡(luò)開銷,而一次性批量插入又鐵定逃不開內(nèi)存占用(其實你想redis擴展要發(fā)送這個批量的指令給redis,那么這塊內(nèi)存肯定是要的),比較好的方式就是分組了,每1000個或者10000個合并成一次$redis調(diào)用,mysql也好其他場景也是類似的

2018年2月3日 09:21
編輯回答
網(wǎng)妓

您說的這個問題 , 我也知道 . 但沒有深究 . 借著這次機會 , 工作完了后一會兒我也深究一下 .
最簡單 , 可以先用 memory_get_usage 來簡單測試一下 .

感謝補刀

2017年3月31日 06:06