MATLABで無理やり参照渡しをするとどうなるのか?


やあ (´・ω・`)

ようこそ、バーボンハウスへ。
このテキーラはサービスだから、まず飲んで落ち着いて欲しい。

うん、「また」なんだ。済まない。
仏の顔もって言うしね、謝って許してもらおうとも思っていない。

でも、このスレタイを見たとき、君は、きっと言葉では言い表せない
「ときめき」みたいなものを感じてくれたと思う。
殺伐とした世の中で、そういう気持ちを忘れないで欲しい
そう思って、このスレを立てたんだ。
じゃあ、注文を聞こうか。

ご注文

なるほど。
じゃあ今から手探りで怪文書を作成するといたします。

参照渡しを使った再帰処理

C++ではこういうふうにかくかなと思います。
メインで宣言されたiのアドレスを関数が受け取って、その関数に対して処理するみたいな。

main.cpp

#include<iostream>

int decrement(int& num)
{
    if (num < 0)
        //再帰処理を終了します
        return 0;

    std::cout << num << std::endl;
    num = num - 1;

    //自分自身を呼び出します
    decrement(num);
    return 0;
}

int main(void)
{
    int i = 10;
    decrement(i);
    td::cout << i << std::endl;
    return 0;
}
実行結果
10
9
8
7
6
5
4
3
2
1
0
-1

最後に-1が出てるので呼び出しのi自体が変更されていることがわかります。

単純なデクリメントをする場合であれば値渡しでもOKなんですけど、
引数のコピーが関数呼び出し毎にされてしまうので、無駄に処理時間がかかります。
また、後で使うんで10という数字が勝手にかわると困る!場合は使っちゃだめですけど、
i変数自体をデクリメントしたい場合、参照渡しで処理するのがいいと思っています。

MATLABは基本値渡し

MATLABは基本的に値渡しです。
こちらに"MATLAB では、C++ などの言語のような、値への参照の定義方法が提供されていません。"と記述があります。
https://jp.mathworks.com/help/matlab/matlab_prog/avoid-unnecessary-copies-of-data.html

どうやら使用される時点でコピーするようですね。MATLABは関数が呼び出されるとその関数のスコープで変数をWORKSPACEに置いて処理するから、
引数の値を関数のWORKSPACEにコピーしないといけないようです。
逆に言えば値渡しでやってるからデバックがしやすいということなんでしょうか。

C++と同じようなのを作ってみたのですが、最後の値が10になっていて、もとの変数iを操作していないのがわかります。

decrement.m
function decrement(num)

    if(num < 0)
        %再帰処理を終了します
        return;
    end
    disp(num);
    num = num - 1;

    %自分自身を呼び出します
    decrement(num)
    return
end

main_script.m
i = int32(10);
decrement(i);
disp(i)

>> main_script
   10
   9
   8
   7
   6
   5
   4
   3
   2
   1
   0
   10

じゃあMATLABで参照渡しはできないんでしょうか?
できるらしいんですよね。
今回の記事はそれを実際にやってみてどうなるかってのをやってみます。

Handleオブジェクトを使う

先程のヘルプ記事の一番下に、ハンドルオブジェクトというものが紹介されていました。これによると、
同じハンドルのコピーを保持するすべての変数は、基になる同じオブジェクトにアクセスし、変更できます
とのこと。つまり参照渡しみたいな感じってこと?
とにかくやってみよう。

numクラスを定義してやってみる

変数を保持するクラスを定義して、handleクラスを継承してあげれば、こいつもhandleオブジェクトの特性を継承するはずです。早速クラスを定義してみましょう。

num.m
classdef num < handle
    properties
        Value;
    end%properti
    methods
        function obj = num(i)
            obj.Value = int32(i);
        end%constructor
    end%method
end

なるほど。 でそんでもってこういうふうに使えば・・・

sansyou_main.m
i = num(10);
decrement(i.Value);
disp(i.Value)
 >>sansyou_Main
   10
   9
   8
   7
   6
   5
   4
   3
   2
   1
   0
   10

あれ!あかんがな!

参照渡しできるのはhandleオブジェクトだけ

ちゃんと書いてあったのに読んでなかったのですが
これはiのメンバである変数を投げてしまったのでこういう結果になったようです。ということはdecrementがiを受け取るようにすればいいのか。

decrement_h.m
function decrement_h(num)

    if(num.Value < 0)
        %再帰処理を終了します
        return;
    end
    disp(num.Value);
    num.Value = num.Value - 1;

    %自分自身を呼び出します
    decrement_h(num)
    return
end

よし、じゃあスクリプトを書き換えて・・・

sansyou_main.m
i = num(10);
decrement_h(i);
disp(i.Value)
 >>sansyou_Main
   10
   9
   8
   7
   6
   5
   4
   3
   2
   1
   0
   -1

おーーーー!ついにやりました!

できるのはわかったけど実際どうなの?

基本的にはHandleクラスはGUIプログラミングを想定して作られたクラスのようなので、なにか値だけ渡すためにわざわざ使うこともない気もします。言語にはそれぞれ得意不得意があるし、参照渡しを使うかどうかも「やりたいこと次第」なので、それが達成できればどっちでもいいんじゃない?という感じです。ただ、気になるのはコピーのオーバーヘッドが減って参照渡しのほうが早くなるのか?というところです。

C++だともちろん早くなりますが、MATLABの参照渡しはややトリッキーなので逆に遅くなる、変わらないという結果もあるんじゃないかという気がします。

計測してみた。

matlabには実行時間を計測するtic tocという関数があります。
https://jp.mathworks.com/help/matlab/ref/tic.html#bswc7ww-1-timerVal
これを使ってこんなふうに書いてみました。

count_num = 1000;

tic
i = int32(count_num );
decrement(i);
disp(i)
toc


tic
i = num(count_num );
decrement_h(i);
disp(i.Value);
toc

さぁ結果は・・・・・

%値渡し:経過時間は 0.006534 秒です。
%Handle渡し:経過時間は 0.009766 秒です。

なんですと。。。
あくまで私の環境下かつ、このスクリプトを動かしたときということなので他の条件ではわかりませんが。
値渡しのほうが早いという結果をえました。
%MacBook Pro(13-inch,2019,Two Tunderbolt3 ports)
%1.4GHz QuadCore Intel Core i5
%8GB 2133MHz LPDDR3

結論

MATLABだったら素直に値渡しでいい。

しかし本当かなぁ、この結果にはいろんな要因が絡んでそうですが何がどう結果に絡んでるかわからんので統制が取れてない気もします。
うーむ。
【追記】
いや、俺は大本の値を更新したいんだ!できないって言われても!
ということでであればこんな感じでしょうか。
演算結果をリターンして戻していくスタイル。これもコピーの連続で時間かかりそう・・・

decrement_ret
function return_num = decrement_ref(num)

    if(num < 0)
        return_num = num;
        %再帰処理を終了します
        return;
    end
    disp(num);
    num = num - 1;

    %自分自身を呼び出します
    return_num = decrement_ref(num);
    return
end
hikaku2

count_num = 10;

tic
i = int32(count_num );
decrement(i);
disp(i)
toc

tic
i = int32(count_num );
i = decrement_re(i);
disp(i)
toc
>> hikaku2
   10
   9
   8
   7
   6
   5
   4
   3
   2
   1
   0
   10

経過時間は 0.000408 秒です。
   10
   9
   8
   7
   6
   5
   4
   3
   2
   1
   0
   -1

経過時間は 0.000374 秒です。

あれ、どういうことだってばよ・・・
count_num = 1000;にすると・・・

   (省略)
   1
   0
   1000
経過時間は 0.007106 秒です。
  (省略)
   1
   0
   -1
経過時間は 0.006534 秒です。

これだと早くなるの?ちょっと早くなる理由がわからない・・・・

【追記終了】

なんだか追記で新たな謎を見つけてしまった気がしますが
これはいずれまたの機会にお話することにいたしましょう☆

ただ、少なくともHandleクラスだと大本が参照される挙動についてよくわかったので個人的には勉強になりました。

このケースのみで判断するのも良くない気もするんで
もしお時間あればこの記事をお読みになって試してみたけど逆の結果だった!とかあればおしえていただきたいです!

最後まで読んでいただき ありがとうございました!

記事で作ったスクリプトはこちらにあります。R2020aで作成しております。めっちゃどうでもいいやつをわざわざあげてすんません。
https://github.com/griffin921/SansyouWatashi