エンジニアはフリーランスになったほうが良い

掲題の話。ちょっと誇大広告入っていますが、ちょっと思うところもあってまとめてみようと思います。

より正確に言うと別にフリーでなくても独立しなくても、会社員という立場でも良い。
なんだけど大事なのは

常に自分にとって良い仕事を追求し続けることを忘れないこと。
そして今の環境が不適切だと感じたら次のステップへ移動する時期であり、迷わず移動すること。

を心がけていたほうがよい。
逆に会社員でも社内にそういった仕組みを取り組めている会社もあり、そういった会社は非常に良い環境づくりに成功しているといえる。

というか裏っ返すと技術一本で食っていくのであれば、食べるのに困らないように自分でカバーする次世代のドメインを意識し、腕を磨き続ける必要があるというネガティブな側面にも置き換えられるのですが。

個人的には上記のテーマをコンスタントに実現していくためにはフリーランスという独立した立場が一番やりやすいと思う。
その点も踏まえて幾つか実体験と自分の考えをもとにフリーランスになったほうが良い理由を展開していこうではないか。
今まさに会社の中でくすぶっている人、独立しようか考えている人などの参考になればなと思う。

0. 自分の経歴

と、実体験の伴ってない主張はあまり信頼できるものでもない気がするので自分の経歴でも紹介しようと思います。
働き出してからはエンジニア一色。ただ転職回数は同年代にしてはかなり多いと思います。
5年で5社くらいに所属しており。(これは社員として所属。です)
ペース的には1年で一社ペース。これは驚異的な飽きっぽさ。だと思います。
国内だけでなくフィリピンでエンジニアとして働いた実績もあり、日本人とフィリピン人の能力差、価格差なども見てきました。

今現在はフリーランスとして活動しており、メインで1箇所の現場に常駐して日中働く形で、1年程度同じ現場に常駐しています。
また合わせて幾つか他の現場でも仕事を走らせているスタイルをとっています。

自分自身もう社員として雇われようという気は毛頭なく、今後はしばらくフリーランスまたは起業という形で仕事を進めていきたいと思っています。
そのほうが合理的ですしね。

さて、このように一応ひととおり経験した中でメリットと思っていることを列挙していきましょう。

1. 給与基準が高い。そして明確

まずはこれ、ここで言うのは一般的な市場の話なんだけれども、フリーランスのほうが頭一個抜いて給与が高い。
そもそもの話、フリーエンジニアを受け入れることができる企業というのは、すでに体力がある企業だけだから、という話でもある。

対照的にもう悲惨としか言いようがないのが、SESをメインとして行っている会社である。
この場合従業員は作業者として、他の会社に対して常駐し、業務を遂行することで所属会社に対して利益を発生させる。
当たり前なんだけど会社としては発生している利益以上に、社員に給料を発生させる訳にはいかない。
ということで社員からしてみると実際に自分が稼いでいる単価の大部分を会社に納めて、残りカスをもらっているような形になる。
・・・いやもうこれわけわかんないよね。
さっさと直接契約したほうがよいのである。だってほぼ全部自分でもらえるようになるんだもん。

おんなじ仕事をしているにもかかわらず給料が違ったら、選択は明確ですよね。

またその給与基準としては、仕事ができればよいのである。
面倒くさい人間関係とかなく、入社後に上司に気をもむ必要もなく、とにかく作業者としての実績を上げていけばそれなりに給与が上がる。

社員として入社すると、まずは訳の分からない今期目標とかたてさせられ、終わったら振り返りだのなんだの。
結果的に達成度がこのくらいだから、今期はこのくらいの査定だね。なんてやりとりもない。
そんなものは自分で管理すれば良い。なぜいちいち上長とすり合わせる必要があるのか。
こんな上長の能力に左右される運ゲーするくらいなら自分の能力だけで勝負していったほうがよっぽど合理的であるし、結果的に評価される。

また会社というのはどうしても大人数を評価する際に、賃金テーブルのような評価制度を導入する。
そのため社員として評価を上げたとしても、そのテーブルに沿ってしか変化しないことが多い。
対してフリーであれば成果を、社外にも展開することで、再利用可能な評価へと昇華することができる。
社会全体を横断的に移動しやすいのである。(これは意識的に自分でメディアを活用していくとやりやすい。)

2. 自分で現場を選ぶことができる利益

まずはいまの日本はかなりの売り手市場だ。優秀なエンジニアはどの企業でもほしがっており、まず選ぶ権利がある。
自分の経歴でも紹介したが、短期間で転職したとしても、能力さえあれば職にはつける
(ただし、転職を繰り返すと別の面が心配され雇用されなくなるだろう。汗)
これが他の業種だったら今頃私はコンビニのバイトにでもなっていると思う。

また自分が足りないと思っている技術を補ったり、伸ばしたい技術があったり、今後伸びると踏んでいる技術があった場合現場を変えれば良い。
またこれは給与でも同様であり、とりあえず必要なスキルセットで金がほしい場合、これも環境を変えれば良い。

また家庭があり仕事よりも、嫁や子どもと一緒にいたいと思うのであれば、中にはリモートOKな現場もある。
田舎でのんびりしながら働きたいのであればそれもよいでしょう。

幸せを追い求めて働き方を選べるというのは大きいと思う。

3. 複数の仕事を走らせられる

常に自分で仕事をある状態にしておくことで結果的にリスクを避けられる。
複数の掛け持ちをしていれば、仮に一つの仕事で失敗してもリスクは抑えられるでしょう。
会社員では副業禁止。などとくだらない制約を設けている企業もあり。フリーならば縛られることもない。

またいろいろな現場で働くことによって自分の価値を認識できるのも良い所です。
今の自分のどのくらいのパワーで、どのくらいお金を稼げるか。
これは逆に繰り返すと仕事を依頼する側になった時のバロメータにもなる。

仕事を納品ベースの仕事に切り替えれば、爆発的に売上を上げることも可能でしょう。
その場合各仕事で繁忙期をうまくずらすようにしてスケジューリングすればよい。
こういう働き方のチョイスは常駐型の社員としてでは無理な選択です。

4. いつでもやめられる

社員でもやめられますが、はじめからフリーとしてスタイルを構築しておいてリスクヘッジをとっておけば。
次の仕事を見つけるのはたやすいです(というかそういうふうに作っておく)

嫌ならやめたらいいんよ。人生休みたい時もあるし。
定期的なinputの時間を取りたいときもあり。集中したい活動があればそれを優先するべきである。
これって人生の本質だよね。仕事をするのは稼がなければならないわけですが、仕事をするために生きているわけではない。

5. 楽しい

とにかく楽しい。なんでも自分の好きなようにできることが楽しい。

どうでしょうか。当たり前ですが別にフリーにならなくても良いと思います。人それぞれですから。
また冒頭でも述べましたが、中には会社内部でうまく社員を満足させるように仕組みを作っているところもいっぱいあります。
参考になればと思います。

また質問・相談などあれば寄せていただければ。
一人でも多くの人の生き方の参考になればいいなと思います。


Cのソケット実装を追う

最近TokyoTyrant、TokyoCabinetに触る機会があって、その実装レベルまで込み入った話に接する機会があった。
本日は最近気になっているTokyoTyrant, TokyoCabinetに関するモジュールのソースを追って、C言語で通信をどのように実装しているのかを確認してみたいと思います。

さてちょっといきなり飛ぶが、tokyotyrantのインタフェースから各種のメソッドを呼び出すとき
当然初めにリモートホストとの接続リソースを生成する必要がある。そこから始めよう。

tokyotyrantでは接続リソースを生成する際に下記のtcrdbopenを利用する。

/* Open a remote database. */
bool tcrdbopen(TCRDB *rdb, const char *host, int port){
  assert(rdb && host);
  if(!tcrdblockmethod(rdb)) return false;
  bool rv;
  pthread_cleanup_push((void (*)(void *))tcrdbunlockmethod, rdb);
  rv = tcrdbopenimpl(rdb, host, port);
  pthread_cleanup_pop(1);
  return rv;
}

リモートリソースをオープンするときtcrdblockmethodを使用。
ロックの取得を行っている。

/* Lock a method of the remote database object.
   <code>rdb' specifies the remote database object.
   If successful, the return value is true, else, it is false. */
static bool tcrdblockmethod(TCRDB *rdb){
  assert(rdb);
  if(pthread_mutex_lock(&amp;rdb-&gt;mmtx) != 0){
    tcrdbsetecode(rdb, TCEMISC);
    return false;
  }
  return true;
}

実態は何をやっているかというと下記メソッドを実行している
pthread_mutex_lock(&rdb->mmtx)

&rdb構造体を確認すると

typedef struct {                         /* type of structure for a remote database */
  pthread_mutex_t mmtx;                  /* mutex for method */
  pthread_key_t eckey;                   /* key for thread specific error code */
  char *host;                            /* host name */
  int port;                              /* port number */
  char *expr;                            /* simple server expression */
  int fd;                                /* file descriptor */
  TTSOCK *sock;                          /* socket object */
  double timeout;                        /* timeout */
  int opts;                              /* options */
} TCRDB;

各種メソッドのためのmutexロックとある
ところでさらにpthread_mutex_tという型が何なのか。

pthread_mutex_tとはtokyotyrantにおける拡張ではなくc言語で用意されている機構である。
manpageは下記になる。

http://linuxjm.sourceforge.jp/html/glibc-linuxthreads/man3/pthread_mutex_lock.3.html

pthread_mutex_lockはmutexロックを取得しようとする。mutexロックはスレッドの間で共通のロックを一つだけ持ち共有する。
ということはプロセス間ではそのロックは有効ではないので、その場合はまた別のロック機構が必要となる。

またpthread_mutex_lockはロックを取得できれば、直ちにロックを返却し、ロックが取得できなかった場合はロックが取得できるまでスレッドの実行を停止させるなどの挙動を示す。
ここですでにロックを取得していたスレッドがいた場合の挙動は、mutexを初期化した際の状態に依存して異なる。
tcrdbの初期化処理を見てみよう。

/* Create a remote database object. */
TCRDB *tcrdbnew(void){
  TCRDB *rdb = tcmalloc(sizeof(*rdb));
  if(pthread_mutex_init(&rdb->mmtx, NULL) != 0) tcmyfatal("pthread_mutex_init failed");
  if(pthread_key_create(&rdb->eckey, NULL) != 0) tcmyfatal("pthread_key_create failed");
  rdb->host = NULL;
  rdb->port = -1;
  rdb->expr = NULL;
  rdb->fd = -1;
  rdb->sock = NULL;
  rdb->timeout = UINT_MAX;
  rdb->opts = 0;
  tcrdbsetecode(rdb, TTESUCCESS);
  return rdb;
}

するとpthread_mutex_init関数を用いて、第二引数がNULLで初期化されていることがわかる。
pthread_mutex_initのmanpageは下記にある

http://linuxjm.sourceforge.jp/html/glibc-linuxthreads/man3/pthread_mutexattr_init.3.html

マニュアルを拝見すると第二引数はpthread_mutexattr_tという属性を指定することになる。
これがNULLである場合代わりにデフォルトの属性が使用される。
デフォルトのmutex種別は「速い(fast)」である。

さて、先のコードに戻って、このfast種別の場合、ロックを取得しようと場合どうなるかというところに話を戻そう。
mutexがfastの場合、すでにロックがあるmutexを取得しようとした場合呼び出しスレッドを永遠に停止させる。
ちなみにpthread_mutexattr_initの返り値の定義に関しては常に0を返却する。とある。
これはつまりtcrdblockmethodメソッドは、純粋にロックを直列に管理するような機構になっている事がわかる。
0でない場合雑多なエラーとして返却しているが、これはプログラムとして異常な状態になっていることを管理する機構であると推測される。

さて無事にほかスレッドとの競合に打ち勝ってロックが取得できたところで、再びtcrdbopenに戻ってみよう。

/* Open a remote database. */
bool tcrdbopen(TCRDB *rdb, const char *host, int port){
  assert(rdb && host);
  if(!tcrdblockmethod(rdb)) return false;
  bool rv;
  pthread_cleanup_push((void (*)(void *))tcrdbunlockmethod, rdb);
  rv = tcrdbopenimpl(rdb, host, port);
  pthread_cleanup_pop(1);
  return rv;
}

次にpthread_cleanup_pushという関数を呼び出している。これを見てみよう。
マニュアルによると、スレッドがキャンセルされた場合の、ハンドラメソッドを追加するとある。
C言語では作成したスレッドは明示的に終了することができ、その際にpthread_cleanup_pushで登録したハンドラが実行される。
この場合tcrdbunlockmethodが実行され、その時の引数としてrdbを渡すということを宣言している。

ここでtcrdbunlockmethodを見てみよう

/* Unlock a method of the remote database object.
   </code>rdb' specifies the remote database object. */
static void tcrdbunlockmethod(TCRDB *rdb){
  assert(rdb);
  if(pthread_mutex_unlock(&amp;rdb-&gt;mmtx) != 0) tcrdbsetecode(rdb, TCEMISC);
}

非常にシンプルでmutex_lockを開放していることがわかる。

さて再びtcrdbopen関数に戻って、tcrdbopenimplを参照してみよう。

/* Open a remote database.
   <code>rdb' specifies the remote database object.
   </code>host' specifies the name or the address of the server.
   `port' specifies the port number.
   If successful, the return value is true, else, it is false. */
static bool tcrdbopenimpl(TCRDB *rdb, const char *host, int port){
  assert(rdb &amp;&amp; host);
  if(rdb-&gt;fd &gt;= 0){
    tcrdbsetecode(rdb, TTEINVALID);
    return false;
  }
  int fd;
  if(port &lt; 1){
    fd = ttopensockunix(host);
  } else {
    char addr[TTADDRBUFSIZ];
    if(!ttgethostaddr(host, addr)){
      tcrdbsetecode(rdb, TTENOHOST);
      return false;
    }
    fd = ttopensock(addr, port);
  }
  if(fd == -1){
    tcrdbsetecode(rdb, TTEREFUSED);
    return false;
  }
  if(rdb-&gt;host) tcfree(rdb-&gt;host);
  rdb-&gt;host = tcstrdup(host);
  rdb-&gt;port = port;
  rdb-&gt;expr = tcsprintf(&quot;%s:%d&quot;, host, port);
  rdb-&gt;fd = fd;
  rdb-&gt;sock = ttsocknew(fd);
  return true;
}

細かなエラーチェックは割愛し、本体を見てみると
port番号が1未満、これを想定しているのはおそらく未指定であった場合であるが、そのときはttopensockunix関数を呼び出している。
またportが指定されていた場合はttopensockが呼び出されている。これらについて追っていこう。

初めにttopensockunixから参照してみる。

/* Open a client socket of UNIX domain stream to a server. */
int ttopensockunix(const char *path){
  assert(path);
  struct sockaddr_un saun;
  memset(&saun, 0, sizeof(saun));
  saun.sun_family = AF_UNIX;
  snprintf(saun.sun_path, SOCKPATHBUFSIZ, "%s", path);
  int fd = socket(PF_UNIX, SOCK_STREAM, 0);
  if(fd == -1) return -1;
  int optint = 1;
  setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, (char *)&optint, sizeof(optint));
  struct timeval opttv;
  opttv.tv_sec = (int)SOCKRCVTIMEO;
  opttv.tv_usec = (SOCKRCVTIMEO - (int)SOCKRCVTIMEO) * 1000000;
  setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (char *)&opttv, sizeof(opttv));
  opttv.tv_sec = (int)SOCKSNDTIMEO;
  opttv.tv_usec = (SOCKSNDTIMEO - (int)SOCKSNDTIMEO) * 1000000;
  setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (char *)&opttv, sizeof(opttv));
  double dl = tctime() + SOCKCNCTTIMEO;
  do {
    int ocs = PTHREAD_CANCEL_DISABLE;
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &ocs);
    int rv = connect(fd, (struct sockaddr *)&saun, sizeof(saun));
    int en = errno;
    pthread_setcancelstate(ocs, NULL);
    if(rv == 0) return fd;
    if(en != EINTR && en != EAGAIN && en != EINPROGRESS && en != EALREADY && en != ETIMEDOUT)
      break;
  } while(tctime() <= dl);
  close(fd);
  return -1;
}

sockaddr_unという構造体が出現しているが、これはUNIX LOCALなソケットのアドレスの情報を表す構造体である。
通信のためのfd(ファイルディスクリプタ)を取得するソケット生成の実態はscoket関数で行われている
引数がPF_UNIX, SOCK_STREAM, 0となっているが順に説明する。

PF_UNIXはソケット通信にファイルを用いることを示す。
詳しくは割愛するが、UNIXのローカルのソケット通信にはファイルを用いたものとTCP通信を用いたものがある。
またSOCK_STREAMに関してはソケットのタイプを表しており、これも詳しくは割愛するが
UNIXのソケットタイプにはSOCK_STREAMとSOCK_DGRAMが存在する。

ちなみにドメインソケットのアドレスは下記の構造体でシンプルに示される

#define UNIX_PATH_MAX    108

struct sockaddr_un {
    sa_family_t sun_family;               /* AF_UNIX */
    char        sun_path[UNIX_PATH_MAX];  /* pathname */
};

また作成したソケットに対してオプション設定している箇所がある。
SOL_SOCKETはオプションの層を表す識別子で、ソケットAPIそうでそうでオプションを有効にすることを表す。
SO_RCVTIMEO, SO_SNDTIMEOはそれぞれ受信、送信のタイムアウト設定をしている。これを超えた場合エラーが送信される。

最終的にconnectシステムコールを要求してファイルディスクリプタが参照しているソケットを引数で渡しているアドレス空間が示す場所とに連結してます。
この一連の処理によってサーバと接続されたファイルディスクリプタが取得でき、それによって通信することができるようになるわけですね。

これ以降の深いレイヤについてはUnixソースを参照していくと良いと思います。

さて対してリモートサーバと通信する場合はどうしているのかを追っていきましょう。

/* Open a client socket of TCP/IP stream to a server. */
int ttopensock(const char *addr, int port){
  assert(addr && port >= 0);
  struct sockaddr_in sain;
  memset(&sain, 0, sizeof(sain));
  sain.sin_family = AF_INET;
  if(inet_aton(addr, &sain.sin_addr) == 0) return -1;
  uint16_t snum = port;
  sain.sin_port = htons(snum);
  int fd = socket(PF_INET, SOCK_STREAM, 0);
  if(fd == -1) return -1;
  int optint = 1;
  setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, (char *)&optint, sizeof(optint));
  struct timeval opttv;
  opttv.tv_sec = (int)SOCKRCVTIMEO;
  opttv.tv_usec = (SOCKRCVTIMEO - (int)SOCKRCVTIMEO) * 1000000;
  setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (char *)&opttv, sizeof(opttv));
  opttv.tv_sec = (int)SOCKSNDTIMEO;
  opttv.tv_usec = (SOCKSNDTIMEO - (int)SOCKSNDTIMEO) * 1000000;
  setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (char *)&opttv, sizeof(opttv));
  optint = 1;
  setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (char *)&optint, sizeof(optint));
  double dl = tctime() + SOCKCNCTTIMEO;
  do {
    int ocs = PTHREAD_CANCEL_DISABLE;
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &ocs);
    int rv = connect(fd, (struct sockaddr *)&sain, sizeof(sain));
    int en = errno;
    pthread_setcancelstate(ocs, NULL);
    if(rv == 0) return fd;
    if(en != EINTR && en != EAGAIN && en != EINPROGRESS && en != EALREADY && en != ETIMEDOUT)
      break;
  } while(tctime() <= dl);
  close(fd);
  return -1;
}

ところどころがTCP通信に対応するような設定になっているだけで、基本的な内容はほぼ同じになっています。
sockaddr_inについても下記のような定義になっており、ポート番号が増えているなどTCPレイヤに対応していることが予想できます。

struct sockaddr_in {
    uint8_t sin_len;
    sa_family_t     sin_family;
    in_port_t       sin_port;
    struct  in_addr sin_addr;
    char    sin_zero[8];
};

こんなかんじにCではsocketを生成しているようですね。
ちなみに生成したfdを元にデータをやりとりする場合sendというシステムコールを利用することになりますが、これはまた後々取り上げたいと思います。


PHPのメモリ節約と参照渡しについて

概要

今回は以前から調べようと思って調べきれてなかった、参照渡しとを行うことでメモリの節約をできるのかということについてまとめたいと思います。

トピックとしては下記のような。

  • コピーオンライト
  • 参照カウンタ、参照フラグ

トピックおさらい

PHPでは代入を行う際に基本的に何もしなければ値をコピーして渡します。
参照を渡したい場合は&をつけることによって実現します。

$a = 'str';
$b = $a;
$c = &$a;

例えば上記のようなプログラムを作成した場合。
$bには$aのコピーが作成され、$cには$aの参照が渡されます。

ここで、「$bを使用することで、$aのコピーが作成されてしまうということはメモリが無駄になる。$cのように参照で保持しておいたほうがメモリの節約につながるのではないか」と思う方もいらっしゃるかもしれません。

今日はこの辺について考察していきます。

さて、私の考察の結論から先に書いてしまいますと「適切に参照渡しを使用することで、メモリの節約につながる」と思います。
また合わせて「特に必要がなければ参照渡しを行う必要はない。これによってメモリが無駄になることもない」とも思っています。

具体的に参照渡しするべき場所として考慮すべき時は

  • 巨大な構造を保持しているためメモリの消費を抑えたい
  • 参照元の変数(シンボル)と、参照先の変数が変更を共有できる(当たり前ですが)

逆に参照渡ししなくてもメモリの消費につながらない時とは

  • 参照先の変数で値の変更を行わない※超重要

であり、まさにここが今日のメイントピックでもあります。

調査

具体的に内部実装としてどのように先ほどの参照関係を保持しているかというと
PHP内部で参照カウントとよばれる、値(変数コンテナ)を参照しているシンボルやリンクの数(refcount)および、参照フラグという変数コンテナを参照型で保持しているシンボルが存在するかどうかという情報(is_ref)を保持しています。

例えば下記のコードに関して参照カウントと、参照フラグを見てみましょう。

$a = 'str';
$b = $a;
$c = $b;

このとき各シンボルの情報は下記のようになります。

$a => (refcount => 3, is_ref => 0)
$b => (refcount => 3, is_ref => 0)
$c => (refcount => 3, is_ref => 0)

すべての変数が同じ変数コンテナ(’str’)を参照している形になるので、納得ですね。

次に冒頭に設置したコードだとどうなるかを確認してみましょう。

$a = 'str';
$b = $a;
$c = &$b;

非常におどろくべきところなのですが、参照カウントが2となっている変数コンテナと、参照カウントが1となっている変数コンテナにわかれました。

$a => (refcount => 2, is_ref => 1)
$b => (refcount => 1, is_ref => 0)
$c => (refcount => 2, is_ref => 1)

これはどういうことなのかというと、参照渡しによりシンボルに代入を行うと、そのタイミングで別個の変数シンボルが即座に作成されます。
つまり変数コンテナのユニーク性にis_refフラグが関与しているということになります。

上記の情報を元にもう一度、参照渡ししなくてもメモリの消費につながらない時について考えてみましょう。
つまるところ、変数コンテナが作成される条件に注意していけば大丈夫です。

悪いコードとして下記の例を上げましょう(ちょっと無理矢理かな・・・)

// アプリケーションの設定項目なんかを参照で取得
$config = &$app->_config;
...
// 一旦退避とかしてみる
$tmp = $config;

上記の例では$tmpに$configを代入した段階で参照フラグの異なる変数コンテナを生成しています。
このタイミングで一気にドカンと$app_configと参照フラグのみ異なり、内容の全く同じ変数コンテナが生成されメモリを逼迫します。

結論

じゃあどうすればいいのか・・・ということなんですが。

結論、普通に使用していれば大丈夫です

さきほどの例で言うと、下記のように通常参照にすれば良いと思います。

// アプリケーションの設定項目なんかを参照で取得
$config = $app->_config;
...
// 一旦退避とかしてみる
$tmp = $config;
$tmp['url'] = $new_url;

たとえば$app->_configが配列であった場合、配列のデータの持ち方として変数コンテナをネストするような形でちょうど、親子のような関係がうまれます。
このときに上記のように一個の変数をいじるとしても、その末端の変数コンテナのみがコピー・変更されるだけで全体の他の項目は全く影響がないので、そこまでメモリなどについて心配する必要はありません。

問題が起こるのは参照渡しを保持していた時のみということです。

また冒頭のキーワードで示したようにコピーオンライトという方針に則ってメモリの最適化を図っています。
これは変数を単純にコピーした時点においては、元々あった変数の変数コンテナをずっと参照し続けます。
いざ、変更が入った段階で新しい変数コンテナを作成し、参照先をそちらに切り替えるのです。賢いですね。

ちなみにこのコピーオンライトとはプログラミングの実装以外でも、OSのメモリ管理やファイル管理にも用いられているような技術ですね。

本日はそろそろこのへんで。
参照になれば幸いです。

参考にしたページ
PHP本家