みけCATのにっき(仮)
つれづれなるまゝに、日くらし、PCにむかひて、心に移りゆくよしなし事を、そこはかとなく書きつくれば、あやしうこそものぐるほしけれ。
(本当か!?)
出典

デデンネする vs ごちうさ見る

アバター
みけCAT
記事: 6734
登録日時: 13年前
住所: 千葉県
連絡を取る:

デデンネする vs ごちうさ見る

投稿記事 by みけCAT » 8年前

デデンネする』の仕様がひどかったので、(もう3ヶ月くらい前ですが)『ごちうさ見る』を作ってみました。

この記事では、『デデンネする』のどこがひどいかと、それに対する『ごちうさ見る』での対策を紹介します。
なお、検証に使用したブラウザはWindows 7 x64上のFirefox 46.0です。

●重い

『デデンネする』は重いです。
例えば、「のうねんホイミン」と入力して実行すると、『デデンネする』では結果が出るまで自分の環境で約6秒かかりました。
一方、『ごちうさ見る』では結果が出るまで約0.5秒でした。
このとき、どちらも指定の文字列を見ることは出来ませんでしたが、文字数のリミットは『デデンネする』では3万回であるのに対し、
『ごちうさ見る』では10万回に設定しています。
『デデンネする』では文字数が少ないにも関わらず、ずっと重いことがわかります。

なぜ『デデンネする』はこんなに重いのでしょうか?ソースコードを見てみましょう。
これが『デデンネする』の任意の文字列を入力するモードの試行部分のソースコードです。

CODE:

var txt = "";
var count = 0;
var moji = document.getElementById("text").value;
var array = [];

for(var i = 0; i  0 && !str.startsWith(s)) s = s.substr(1); // ターゲットの接頭辞になるまで前から削る
		judgeNext[i][j] = s.length; // 接頭辞の長さを保存する
	}
}
// 試行する
var result = "";
var resultStr = "", resultStr2 = "";
var count = 0;
var judgeCount = 0;
while (count  0 && count % charPerLine === 0) result += "\n";
	result += char;
	// 状態を進める
	count++;
	judgeCount = judgeNext[judgeCount][num];
	if (judgeCount >= str.length) break;
}
この判定用のテーブルには、指定の文字列の注目している位置と入力された文字から、次の文字がどこにマッチする可能性があるかを格納します。
例えば、指定の文字列が「ゆるゆり」だったとします。

最初に注目するのは、最初の「ゆ」です。
この状態で「ゆ」が入力されると、最初の「ゆ」とマッチしたので、次の「る」に注目します。
また、「る」が入力された場合は、マッチしないので、また最初の「ゆ」に注目します。

探索が進み、最後の「り」に注目している状態になったとします。
このとき、既に「ゆるゆ」にはマッチしています。
この状態で「り」が入力されると、「最後の文字の次の文字」に注目することになり、指定の「ゆるゆり」が現れたことがわかります。
また、「る」が入力された場合は、入力が「ゆるゆる」であったことがわかるので、「ゆる」の次の「ゆ」に注目します。

このように注目している位置と入力された位置からわかる、一番マッチしている状態に遷移します。
指定の文字列は十分短いことを仮定しているので、このテーブルは愚直な方法で計算してかまいません。

●判定が不正確

前述の通り、『デデンネする』ではmatch関数を用いて判定を行っています。
その結果、正規表現が有効になってしまい、例えば.を用いた文字列の出現判定を間違う可能性が生じます。
dedennnesuru-miss-20160429.png
正規表現のメタ文字を含む文字列を入力すると失敗するの図
dedennnesuru-miss-20160429.png (21.8 KiB) 閲覧数: 961 回
また、表示に利用するHTMLを含めて検索してしまっているため、そのHTMLに含まれる文字列を指定すると誤検出してしまいます。
dedennnesuru-miss-20160429-2.png
ヘッダに含まれる文字列を入力すると失敗するの図
dedennnesuru-miss-20160429-2.png (21.4 KiB) 閲覧数: 957 回
もちろん、『ごちうさ見る』ではこのような誤判定は起こりません。

●HTML入力への耐性が無い

『デデンネする』に「たすけてくれ>a」と入力して実行すると、「警告:応答のないスクリプト」のダイアログを何度も出しながら、約2分30秒かかりました。
これは、途中経過の文字列を毎回innerHTMLに設定している上、HTMLの特殊文字への対策が全く無いため、
HTMLのパースにリソースを取られているためであると推測できます。
もちろん、『ごちうさ見る』ではこれらの入力に対してもほとんど変わらない時間で実行できます。

●結果の再現性が無い

『デデンネする』には結果をツイートする機能がありますが、全く証拠が無いため、この結果は簡単に捏造できます。
スクリーンショットを添付しても、そんなものは開発者ツールで簡単に捏造できるので証拠になりません。

一方、『ごちうさ見る』ではシードをランダムに生成し、それを用いて擬似乱数生成器で系列を生成するため、
結果の再現と共有が可能です。もちろんシードを恣意的に指定することも可能ですが、何も証拠が無いよりはいいでしょう。

[hr]
このように、デデンネやエモンガをするだけならいいかもしれないですが、任意文字列入力に対する『デデンネする』の仕様はひどいようです。
最後に、そんな『デデンネする』にいくつかアドバイスを。
  • matchを使わず、生成した文字列の最後をsubstringで取り出して比較するといいでしょう。
  • HTML特殊文字は試行の前に弾きましょう。
  • 無駄なループを消し、出た乱数を直接配列の添字に使いましょう。
  • 表示用のHTMLと生成される系列を分離しましょう。
  • 毎回innerHTMLを更新するのをやめるか、途中経過が見える演出にしましょう。
  • 「デデンネする」「エモンガする」を別のコードで扱わず、任意入力のコードを関数化して使いまわしましょう。
特に、matchからsubstringに変えることでパフォーマンスの改善が見込まれます。
実際にこのコードをブラウザのコンソールで実行してみました。

CODE:

var str = "のうねんホイミン";
var runNum = 30000;
var res = "";
var start = new Date().getTime();
var cnt = 0;
for(;;) {
	var rnd = Math.floor(Math.random() * str.length);
	res += str.charAt(rnd);
	cnt++;
	if (res.match(str) || cnt >= runNum) break;
}
var elapsedMatch = new Date().getTime() - start;
console.log("match : " + elapsedMatch + "ms, cnt : " + cnt);

res = "";
start = new Date().getTime();
cnt = 0;
for(;;) {
	var rnd = Math.floor(Math.random() * str.length);
	res += str.charAt(rnd);
	cnt++;
	if (res.substring(res.length - str.length) === str || cnt >= runNum) break;
}
var elapsedSubstring = new Date().getTime() - start;
console.log("substring : " + elapsedSubstring + "ms, cnt : " + cnt);
結果は

CODE:

match : 1032ms, cnt : 30000
substring : 152ms, cnt : 30000
となりました。また、runNumを100000にすると

CODE:

match : 14262ms, cnt : 100000
substring : 441ms, cnt : 100000
となり、substringの方が明らかに効率がいいことがわかります。
とはいっても、実際の『デデンネする』で6秒くらいかかったのに対し、match版の30000回実行は1秒くらいしかかかっていません。
試しに文字を追加する部分

CODE:

res += str.charAt(rnd);
を、『デデンネする』風の無駄なループ

CODE:

for (var i = 0; i < str.length; i++) {
	if (i == rnd) res += str.charAt(i);
}
に置き換えてみましたが、結果は

CODE:

match : 1782ms, cnt : 30000
substring : 563ms, cnt : 30000
となり、少ししか差は現れませんでした。
このことから、少なくとも3万回という条件では、matchの負荷よりinnerHTMLの更新(とそれに伴うレンダリング)の負荷のほうが大きいことが予想できます。
従って、どうせ見えないのに無駄にinnerHTMLを更新するのをやめるか、タイマーを使ってきちんと途中経過が見えるようにするべきでしょう。

コメントはまだありません。