見出し画像

ティラノスクリプトで5万倍速くループ処理する方法

数字のデカい煽りタイトルおつかれさまです。でも本当なので最後まで読んでみてください。

まず、この記事を書こうと思った事の発端ですが、先日こういう記事を見掛けまして…

既存のタグを使用すると上記の記事のようなコードが完成形になると思います。
この記事の処理は素晴らしく汎用性も高いです。

しかし、あまり知られていない……というかこれを知っている人はこれからこの記事を読む貴方と、ティラノスクリプトのGitHubを舐め回すように徘徊している筆者のようなごく一部のティラノマニアだけなのですが、実は[jump]タグの動作はティラノスクリプトのタグの中でも物凄く処理の遅いタグなんです。

まずは下記のコードを見てみましょう。
やっていることは5を1000回足すという単純な処理です。

[iscript]
	//1000回足す値 結果的に今回は 5 * 1000 を乗算を使わずループして加算で処理するコード
	tf.val = 5

	//変数の初期化とタイマーの開始
	tf.i = 0
	tf.result = 0
	console.time("test1")
[endscript]

*loop1

	[eval exp="tf.result = tf.result + tf.val"]

[jump target="loop1" cond="++tf.i < 1000"]

[iscript]
	console.timeEnd("test1")
	console.log("計算結果:" + tf.result)
[endscript]

上記のコードを5回ほど回した結果がこれです。

単位がミリ秒なので、全ての結果で5秒以上、なんなら6秒に近い値となっています。結構かかっていますね。
プログラミングを経験している方なら直感的に分かると思いますが、このような単純な演算でこれだけ処理に時間がかかることはあり得ないんです。
それだけ[jump]タグはコストの高いタグというわけです。

では、なぜ[jump]タグはコストが高くなっているのでしょうか?
早速[jump]タグが何をしているのか実際のコードを覗いてみましょう。

//ジャンプ命令
tyrano.plugin.kag.tag.jump = {
    pm: {
        storage: null,
        target: null, //ラベル名
        countpage: true,
    },

    start: function (pm) {
        if (this.kag.stat.hold_glink && !pm.storage && !pm.target) {
            pm.storage = this.kag.stat.hold_glink_storage;
            pm.target = this.kag.stat.hold_glink_target;
            this.kag.stat.hold_glink = false;
            this.kag.stat.hold_glink_storage = "";
            this.kag.stat.hold_glink_target = "";
        }

        var that = this;

        //ジャンプ直後のwt などでフラグがおかしくなる対策
        setTimeout(function () {
            that.kag.ftag.nextOrderWithLabel(pm.target, pm.storage);
        }, 1);
    },
};

コードを見ても何をしているのかわからなくて大丈夫です。
一見、ライン数も少なく複雑な処理もしていないと思われるこのコードですが、致命的に時間のかかる処理が入っています。

そう、setTimeout関数です。

setTimeout関数の説明についてはここではしませんが、とにかく今回の犯人はコイツです。
注釈を見る限り、一定の条件で発生する不具合への対策らしいです。

今回はその不具合に対して関係がなさそうなので犯人を取り除いた、[jump]タグに似た[while]タグを新しく作ってしまいましょう。

	//whileタグを作成
	TYRANO.kag.ftag.master_tag.while = {
		vital: ["target"],
		pm: {},

		start: function(pm) {
            this.kag.ftag.nextOrderWithLabel(pm.target, null);
		},
		
		kag: TYRANO.kag
	};

はい、これでひたすらラベルにジャンプするだけのタグが完成しました。
早速使っていきます。(元のjumpコード冒頭のif文の処理も今回は不要なので削除しています)

;whileタグを追加
*test2
[iscript]
	//変数の初期化とタイマーの開始
	tf.i = 0
	tf.result = 0
	console.time("test2")
[endscript]

*loop2

	[eval exp="tf.result = tf.result + tf.val"]

[while target="loop2" cond="++tf.i < 1000"]

[iscript]
	console.timeEnd("test2")
	console.log("計算結果:" + tf.result)
[endscript]

同じく上記のコードを5回ほど回した結果がこれです。

[jump]タグと比べると5倍も速くなっている!凄い!

……え?たった5倍?
お前やっぱりタイトル詐欺じゃねーか!
と思われるかもしれませんが、まだまだ記事は続くのでお付き合いしてくださると幸いです。

実は[jump]タグだけでなく[eval]タグもコストの非常に高いタグなのです。
試しに先ほどのコードで[eval]タグをコメントアウトした状態で計測してみます。

*loop2

	;[eval exp="tf.result = tf.result + tf.val"]

[while target="loop2" cond="++tf.i < 1000"]

10倍以上違っていますね。
[eval]タグで演算を行うとどうしてもティラノスクリプトの仕様上遅くなってしまいます。ちなみに[iscript]タグで代用しても結果は同じ感じになります。
(実際にはstackエラーが起こりますがここでは解説しません)

じゃあどうやって演算すればいいんだ!!ってなるわけですが、ここで気付けた人は鋭いです。
そう、すでにこのコードではとある方法で演算を行っている部分が存在するんです。

それは[while]タグ内のcondパラメーター。

condはJSの演算結果がtrueの時に実行するパラメーターです。
今回の場合、cond="++tf.i < 1000"が指定されており、これはtf.iをインクリメント(変数の前方に++を記述するとその変数に+1が加算される処理)を実行した後に1000より小さい値だった場合タグを実行する、という命令になります。
つまり、cond内で演算が可能なのです。

次はこの特性を利用して、condのみ使用するための何も実行しない虚無タグを新規に作成します。

	//虚無のnタグを作成
	TYRANO.kag.ftag.master_tag.n = {
		start: function() {
			this.kag.ftag.nextOrder();
		},
		
		kag: TYRANO.kag
	};

この虚無タグにcondを設定して生きる意味を与えてあげたコードが次になります。

;nタグを追加
*test3
[iscript]
	//変数の初期化とタイマーの開始
	tf.i = 0
	tf.result = 0
	console.time("test3")
[endscript]

*loop3

	[n cond="tf.result = tf.result + tf.val"]

[while target="loop3" cond="++tf.i < 1000"]

[iscript]
	console.timeEnd("test3")
	console.log("計算結果:" + tf.result)
[endscript]

[eval]タグで演算を行っていたときは1000ミリ秒以上かかっていたのでこれで5倍くらいまた速くなりました!
ここで一度、今までの結果画像を並べてみます。

左:デフォルト 中:jump未使用 右:eval未使用

最初の状態と比べると約30倍くらい速くなっている!
赤いザクよりも10倍速い!ネタが古い!

……え?話が違うって?もう一度タイトルを振り返ってみろって?

わかりました。もったいぶらずにタイトル通りのコードを書きます。

;iscript内で完結させる
*test4
[iscript]
	tf.i = 0
	tf.result = 0
	console.time("test4")
	
	while (tf.i++ < 1000) {
		tf.result = tf.result + tf.val
	}	
	console.timeEnd("test4")
	console.log("計算結果:" + tf.result)
[endscript]

はい!5万倍速くなりました!!

演算するだけなら最初からiscript内だけで済ませておけ!ってことですね。

やっていることは最初の処理と同じですが、結果はこれで5万倍速くなりました。タイトルは「ティラノスクリプトで5万倍速くループ処理する方法」というより正確には「ティラノスクリプトで5万倍速く演算をループ処理する方法」になるんですが、ギリギリなところでタイトル詐欺ではないと思います。タイトル詐欺じゃないです。タイトル詐欺じゃない。

テスト用に今回の全コードを置いておきます。コピペして適当な箇所に張り付ければ動くと思います。

[iscript]
	//debugMenu.visibleを無効化
	TYRANO.kag.config["debugMenu.visible"] = false
	
	//consoleをクリア
	console.clear()
	
	//1000回足す値 結果的に今回は 5 * 1000 を乗算を使わずループして加算で処理するコード
	tf.val = 5

	//whileタグを作成
	TYRANO.kag.ftag.master_tag.while = {
		vital: ["target"],
		pm: {},

		start: function(pm) {
            this.kag.ftag.nextOrderWithLabel(pm.target, null);
		},
		
		kag: TYRANO.kag
	};

	//虚無のnタグを作成
	TYRANO.kag.ftag.master_tag.n = {
		start: function() {
			this.kag.ftag.nextOrder();
		},
		
		kag: TYRANO.kag
	};
[endscript]

*select
[glink target="test1" text="デフォルト"      size=30  width=500 y=150]
[glink target="test2" text="whileタグを追加" size=30  width=500 y=250]
[glink target="test3" text="cond内で演算"    size=30  width=500 y=350]
[glink target="test4" text="iscript内で演算" size=30  width=500 y=450]
[s]

;デフォルトの状態
*test1
[iscript]
	//変数の初期化とタイマーの開始
	tf.i = 0
	tf.result = 0
	console.time("test1")
[endscript]

*loop1

	[eval exp="tf.result = tf.result + tf.val"]

[jump target="loop1" cond="++tf.i < 1000"]

[iscript]
	console.timeEnd("test1")
	console.log("計算結果:" + tf.result)
[endscript]

[jump target="select"]
[s]


;whileタグを追加
*test2
[iscript]
	//変数の初期化とタイマーの開始
	tf.i = 0
	tf.result = 0
	console.time("test2")
[endscript]

*loop2

	[eval exp="tf.result = tf.result + tf.val"]

[while target="loop2" cond="++tf.i < 1000"]

[iscript]
	console.timeEnd("test2")
	console.log("計算結果:" + tf.result)
[endscript]

[jump target="select"]
[s]


;nタグを追加
*test3
[iscript]
	//変数の初期化とタイマーの開始
	tf.i = 0
	tf.result = 0
	console.time("test3")
[endscript]

*loop3

	[n cond="tf.result = tf.result + tf.val"]

[while target="loop3" cond="++tf.i < 1000"]

[iscript]
	console.timeEnd("test3")
	console.log("計算結果:" + tf.result)
[endscript]

[jump target="select"]
[s]


;iscript内で完結させる
*test4
[iscript]
	tf.i = 0
	tf.result = 0
	console.time("test4")
	
	while (tf.i++ < 1000) {
		tf.result = tf.result + tf.val
	}	
	console.timeEnd("test4")
	console.log("計算結果:" + tf.result)
[endscript]

[jump target="select"]
[s]

繰り返しますが実際は冒頭でリンクしたnoteの記事のような他のタグを併用してループ処理することの方が実用性が高いです。数回~十数回くらいまでのループであればパフォーマンスにも問題はないと思います。
ただ同様の処理をする場合で実行時に、もしなにか引っかかった感じがしたり重く感じた場合は、今回の記事がパフォーマンスチューニングのお役に立てるかもしれません。

また、ループに限らずevalを複数行連続して行っているような処理があればそれを一括でiscriptで処理した方が高速です。

[eval exp="tf.test1 = 1"]
[eval exp="tf.test2 = 2"]
[eval exp="tf.test3 = 3"]
;これを書くなら

[iscript]
    tf.test1 = 1
    tf.test2 = 2
    tf.test3 = 3
[endscript]
;こっちにした方が処理が速いよ

えらく遠回りしながら、実は一番話したかったことはコレだったんですが、他の方が書いたスクリプトを拝見すると、結構上記のようなevalが連続するコードを書かれていることが多々見受けられますので、なるべくiscriptにまとめた方がいいですよって話です。
なら最初からこれだけ書けよ。って突っ込みはやめてください。私も今そう思っています。

今回はちょっとディープな内容になりました。
そんなディープな内容が詰まったスクリプトで組まれたゲームを筆者は公開していますので、よろしければプレイしてみてください!

ゲームを楽しんで貰えたり、記事やプラグインがお役に立てましたらサポート(投げ銭)を頂けると幸いです!