PHP可变长参数(...)和生成器问题
问题是关于向redis快速插入大量数据。我使用了1百万个元素数组来插入，在我这里是内存溢出的，所以我使用了生成器的方式
```
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";
```
输出结果：
```
[vagrant@localhost ~]$ php redis.php 
1.2786898612976
[vagrant@localhost ~]$
```
然后redis-cli中确实有了一百万个元素。那么当我把代码中的一百万修改为一千万的时候又报内存溢出
```
[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
```
根据我理解的生成器知识不应该出现内存溢出的情况。因为自始至终生成器xrange只占用一个变量`($i)`内存?
所以我猜测是不是`$redis->sadd($key, ...$r);`这一步的时候`...$r`依然会解析成大数组。 现在不知道如何证实。
补充：
我在`sadd`之前使用`var_dump(...$r);exit;`发现输出的都是
```
int(999775)
int(999776)
int(999777)
int(999778)
int(999779)
int(999780)
int(999781)
```
这样可以证明生成器确实是一个一个产出值的。那么为什么将...$r传入到sadd()的时候还报内存不足呢？不明白这个`...`的原理，还请大佬们指点。
a. 这个问题和redis毫无关系
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之间，几乎没有内存占用，这是所谓的"生成器节省内存”的现象，也就是各种相关文章里都会解释的，在30行迭代生成器的时候，每次循环都会进到生成器内部去yield一次，产生一个大字符串，下次循环的时候循环变量又重新被赋值，之前的字符串自然会被GC回收，所以无论循环多大多少次，占用的内存是稳定的（包括上面的`$gen=gen()`也是几乎不占内存的）

33-35，无论被调用的函数如何，甚至noop函数，都一样会占用大量内存，占用内存的量明显和次数成正比，也就是说生成器的内容被合并到一起而占用了一整块内存。这其实很容易解释，几乎的所有语言“调用函数”的过程都是类似的
1. 首先计算所有参数，形成参数列表
2. 生成call frame（其中包含调用被调双方、文件行号、参数列表等等信息），压入call stack中
3. 控制权移交给函数内部（当然省略了超级多的细节，比如实参形参的映射/copy啊，内存管理啊等等什么的，和本题无关）
`...$args`这个操作符其实影响的就是第一个阶段，计算参数的时候，看到...操作符，就需要展开其中的参数来形成参数列表，那么用生成器的场合，这个阶段内存就从原有生成器的少量占用变成了完整的占用了，所以即使是空的`noop`函数也会占用几乎一样多的内存，你的理解是正确的

回到原题的那个redis问题的话，因为重复调用redis方法一定会占用大量的额外网络开销，而一次性批量插入又铁定逃不开内存占用（其实你想redis扩展要发送这个批量的指令给redis，那么这块内存肯定是要的），比较好的方式就是分组了，每1000个或者10000个合并成一次$redis调用，mysql也好其他场景也是类似的