質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

Q&A

解決済

3回答

4347閲覧

先頭に指定文字があったら削除を実装する際、正規表現を使用した方が良いか、計測する方法を知りたい

re97

総合スコア208

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

0グッド

0クリップ

投稿2016/03/26 01:40

最終的にやりたいこと
・先頭に指定文字※があったら削除

前提条件
・対象文字列の先頭に※が1回含まれるか、全く含まれないの何れか

分からないこと
・下記何れで実装した方が良い?
案1.先頭一文字を取得後、条件分岐処理
案2.正規表現

質問背景
・正規表現は遅いからなるべく使用しないほうが良い、とどこかで聞いたことがあるため
・一般的にはどちらで実装するのでしょうか?
・あるいはこれら以外?

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答3

0

コード

js

1'use strict'; 2 3[ 4 ['※abc', 'マッチする短い文字列'], 5 ['abc', 'マッチしない短い文字列'], 6 ['※' + new Array(10000).join('a'), 'マッチする長い文字列'], 7 ['' + new Array(10000).join('a'), 'マッチしない長い文字列'], 8].map(function (item) { 9 var i; 10 var str = item[0]; 11 12 console.time('indexOf-' + item[1]); 13 for (i = 0; i < 10000000; ++i) { 14 str.indexOf('※') === 0; 15 } 16 console.timeEnd('indexOf-' + item[1]); 17 18 console.time('RegExp-' + item[1]); 19 for (i = 0; i < 10000000; ++i) { 20 str.match(/^※/); 21 } 22 console.timeEnd('RegExp-' + item[1]); 23 24 console.time('startsWith-' + item[1]); 25 for (i = 0; i < 10000000; ++i) { 26 str.startsWith('※'); 27 } 28 console.timeEnd('startsWith-' + item[1]); 29 30 console.log(); 31 32});

結果

V8系

Node.js
indexOf-マッチする短い文字列: 451.954ms RegExp-マッチする短い文字列: 1168.502ms startsWith-マッチする短い文字列: 656.937ms indexOf-マッチしない短い文字列: 451.098ms RegExp-マッチしない短い文字列: 377.314ms startsWith-マッチしない短い文字列: 673.922ms indexOf-マッチする長い文字列: 458.273ms RegExp-マッチする長い文字列: 1188.736ms startsWith-マッチする長い文字列: 678.872ms indexOf-マッチしない長い文字列: 442.082ms RegExp-マッチしない長い文字列: 377.708ms startsWith-マッチしない長い文字列: 649.069ms
Chrome
indexOf-マッチする短い文字列: 676.688ms RegExp-マッチする短い文字列: 1360.670ms startsWith-マッチする短い文字列: 1462.866ms indexOf-マッチしない短い文字列: 541.262ms RegExp-マッチしない短い文字列: 617.367ms startsWith-マッチしない短い文字列: 851.028ms indexOf-マッチする長い文字列: 636.199ms RegExp-マッチする長い文字列: 1344.160ms startsWith-マッチする長い文字列: 1424.883ms indexOf-マッチしない長い文字列: 506.433ms RegExp-マッチしない長い文字列: 517.285ms startsWith-マッチしない長い文字列: 780.458ms

SpiderMonkey系

Firefox
indexOf-マッチする短い文字列: 284.28ms RegExp-マッチする短い文字列: 3639.21ms startsWith-マッチする短い文字列: 265.69ms indexOf-マッチしない短い文字列: 276.58ms RegExp-マッチしない短い文字列: 1038.85ms startsWith-マッチしない短い文字列: 285.4ms indexOf-マッチする長い文字列: 294.38ms RegExp-マッチする長い文字列: 3774.39ms startsWith-マッチする長い文字列: 303.2ms indexOf-マッチしない長い文字列: 26592.07ms RegExp-マッチしない長い文字列: 1386.18ms startsWith-マッチしない長い文字列: 285.8ms

考察

  • String#matchマッチするときに遅いとなっているのは,返り値の生成にコストがかかるから.
  • String#startsWithは,V8においてはあまり最適化されておらず,String#indexOfよりも低速.一方SpiderMonkeyにおいては最適化されており,安定して高速.
  • String#indexOfは,V8においては最適化されており,安定して高速.一方SpiderMonkeyにおいてはあまり最適化されておらず,「マッチしない長い文字列」において著しく動作が遅くなる傾向にある.

V8のString#indexOfが**「マッチしない長い文字列」に対して高速**な理由が気になりますね.右辺の値を先に見て,それ以上無駄な走査を行わないようにする…という高度な最適化でもやってるんでしょうか.

投稿2016/03/26 11:22

編集2016/03/26 12:07
mpyw

総合スコア5223

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

mpyw

2016/03/26 12:11 編集

でも速度気にするよりもこの場合は短くコードを書けるかどうかを優先したほうがいいと思いますよ.正規表現使うのが一番シンプルです.if文の分岐が要りませんから. '※abc'.replace(/^※/, '') (何も考えずに先客の真似したけどString#matchじゃなくてString#replaceで検証すべきだった
umed0025

2016/03/26 12:17

うちんところでも同じような結果(nodejs,chrome,firefox)がでてて、結構差がでますねぇ。 Win10+Edge(Chakra)の結果をのせておきます。 indexOf 0: 2,950.67 ミリ秒 RegExp 0: 3,006.58 ミリ秒 startsWith 0: 2,326 ミリ秒 indexOf 1: 2,766.215 ミリ秒 RegExp 1: 2,851.62 ミリ秒 startsWith 1: 2,333.92 ミリ秒 indexOf 2: 1,311.535 ミリ秒 RegExp 2: 1,363.3 ミリ秒 startsWith 2: 992.755 ミリ秒 indexOf 3: 69,527.255 ミリ秒 RegExp 3: 1,349.935 ミリ秒 startsWith 3: 1,065.795 ミリ秒
think49

2016/03/26 12:49

> - String#matchがマッチするときに遅いとなっているのは,返り値の生成にコストがかかるから. そんな時の為に `RegExp#test` があります。
raccy

2016/03/26 13:04

単にマッチしているかどうかだけ見るだけなら、RegExp.prototype.test() も比較対象に追加して欲しいです。
raccy

2016/03/26 13:06

あう、think49さんと被った… リロードしてなかったから…
mpyw

2016/03/26 14:27

RegExp#testに関しては「関連検証」のほうにデータがあるので参考にしてください.
re97

2016/03/27 01:53

・みなさん回答ありがとうございます ・そもそもの疑問なのですが、マッチしない文字列も計測しているのはなぜでしょうか? ・正規表現速度を考える場合、マッチしない文字列がある場合も考慮しなければいけない、ということでしょうか? ・もしそうなら、判断基準は「マッチする文字列」と「マッチしない文字列」の合計計測時間がその正規表現に対する性能評価ということになるのでしょうか? ・そんな単純な話ではない?
think49

2016/03/27 03:33 編集

To: re97 さん 「各関数がどのように動作しているのか」を考えてみてください。 例えば、indexOfは文字列を前方から走査し続ける為、文字列が前方一致しなくとも処理を続けます。 コードの書き方によっては与えられた文字列によっては無駄が出来てしまいます。 ですので、「re97さんの要件」と「選択肢となるコード」を比較して出来るだけ要件に合致したコードを選択する必要があります。
re97

2016/03/28 02:50

・コメントありがとうございました ・質問した時点では単純に一般的な記述方法を知りたかったのですが、正規表現を使用する場合においては、要件(対象文字列)に応じて結果は変動するということですね ・「先頭に指定文字があったら削除」という同じ条件でも、渡す文字列内容によって結果は異なることがあり得ると ・なるほど、と思いました
guest

0

ベストアンサー

比較するなら RegExp#test にしないと不公平な気はしますね。

比較検証用コード

JavaScript

1'use strict'; 2function test (target, search) { 3 var i = 50000, 4 regExp = new RegExp('^' + search.replace(/(\W)/g, '\u005C$1')); 5 6 while (i--) { 7 regExp.test(target); 8 } 9 10 return regExp.test(target); 11} 12 13function propertyAccess (target, search) { 14 var i = 50000; 15 16 while (i--) { 17 target[0] === search; 18 } 19 20 return target[0] === search; 21} 22 23function charAt (target, search) { 24 var i = 50000; 25 26 while (i--) { 27 target.charAt(0) === search; 28 } 29 30 return target.charAt(0) === search; 31} 32 33function indexOf (target, search) { 34 var i = 50000; 35 36 while (i--) { 37 target.indexOf(search) === 0; 38 } 39 40 return target.indexOf(search) === 0; 41} 42 43function lastIndexOf (target, search) { 44 var i = 50000; 45 46 while (i--) { 47 target.lastIndexOf(search, 0) === 0; 48 } 49 50 return target.lastIndexOf(search, 0) === 0; 51} 52 53function startsWith (target, search) { 54 var i = 50000; 55 56 while (i--) { 57 target.startsWith(search); 58 } 59 60 return target.startsWith(search); 61} 62 63 64function benchmark (fn, name, target, search) { 65 console.time(name); 66 var result = fn(target, search); 67 console.timeEnd(name); 68 console.log(result); 69} 70 71var matchedString = '※' + Array(50000).join('a'), 72 noMatchString = Array(50001).join('a'); 73 74benchmark(test, 'RegExp#test (matched)', matchedString, '※'); 75benchmark(test, 'RegExp#test (no match)', noMatchString, '※'); 76benchmark(propertyAccess, 'propety access on strings (matched)', matchedString, '※'); 77benchmark(propertyAccess, 'propety access on strings (no match)', noMatchString, '※'); 78benchmark(charAt, 'String#charAt (matched)', matchedString, '※'); 79benchmark(charAt, 'String#charAt (no match)', noMatchString, '※'); 80benchmark(indexOf, 'String#indexOf (matched)', matchedString, '※'); 81benchmark(indexOf, 'String#indexOf (no match)', noMatchString, '※'); 82benchmark(lastIndexOf, 'String#lastIndexOf (matched)', matchedString, '※'); 83benchmark(lastIndexOf, 'String#lastIndexOf (no match)', noMatchString, '※'); 84benchmark(startsWith, 'String#startsWith (matched)', matchedString, '※'); 85benchmark(startsWith, 'String#startsWith (no match)', noMatchString, '※');

検証結果

Google Chrome 49.0.2623.87 m

RegExp#test (matched): 6.640ms true RegExp#test (no match): 3.348ms false propety access on strings (matched): 4.925ms true propety access on strings (no match): 1.420ms false String#charAt (matched): 5.177ms true String#charAt (no match): 2.596ms false String#indexOf (matched): 5.453ms true String#indexOf (no match): 5.050ms false String#lastIndexOf (matched): 5.788ms true String#lastIndexOf (no match): 3.901ms false String#startsWith (matched): 13.675ms true String#startsWith (no match): 4.301ms false

実装

「何を選択するか」はコードの設計指針によりますが、私自身は次の事に気をつけています。

  • アルゴリズムに無駄が無い事
  • 汎用性が高い事

例えば、String#indexOf は対象文字列の先頭から検索してHITした時点で index 値を返す関数です。その為、検索でマッチしないと最後まで検索し続ける事になります。先頭1文字だけ探せば良いところを最後まで探し続けるのは無駄です。
String#indexOf が候補から外れます。

汎用性とは今回の要件だけでなく、広範囲の要件を網羅できる機能を指します。
今回は先頭の1文字だけを検索すればすみますが、先頭の2文字を削除したい場合も対応できる方が汎用性が高いといえます。
string[0], String#charAt は先頭1文字だけが対象なので汎用性が低いといえます。

JavaScript

1function removeStartsWith1 (targetString, searchString) { 2 if (targetString.startsWith(searchString)) { 3 targetString = targetString.slice(searchString.length); 4 } 5 6 return targetString; 7} 8 9function removeStartsWith2 (targetString, searchString) { 10 return targetString.replace(new RegExp('^' + searchString.replace(/(\W)/g, '\u005C$1'), 'g'), ''); 11} 12 13function removeStartsWith3 (textNode, searchString) { 14 if (textNode.data.startsWith(searchString)) { 15 textNode.deleteData(0, searchString.length); 16 } 17 18 return textNode; 19} 20 21console.log(removeStartsWith1('※aaa', '※')); // "aaa" 22console.log(removeStartsWith1('aaa', '※')); // "aaa" 23console.log(removeStartsWith2('※aaa', '※')); // "aaa" 24console.log(removeStartsWith2('aaa', '※')); // "aaa" 25console.log(removeStartsWith3(document.createTextNode('※aaa'), '※')); // "aaa" 26console.log(removeStartsWith3(document.createTextNode('aaa'), '※')); // "aaa"

String#startsWith は IE11- に対応する為に Polyfill が必要なので後述します。
先頭文字列削除に特化するなら String#replace の実装がお手軽だと思います。
対象がテキストノードなら CharacterData.prototype.deleteData でテキストノード自身を操作できるのでお勧めです。

String.prototype.startsWith の Polyfill

String.prototype.startsWith は ES6 規定の為、IE11- で使用できませんが、Polyfill を使うことで対応できます。

結論

いろいろ書きましたが、個人的には String#replaceCharacterData#deleteData の二択ですね。
テキストノードをゴリゴリ操作するなら CharacterData#deleteData、文字列操作なら String#replace というところでしょうか。
テキストノードも data プロパティから String#replace を使用してもいいのですが、画面の再描画コストが CharacterData#deleteData の方が低そうな気がします(未検証)。

参考リンク

(更新履歴)

  • 2016/03/27 11:55 String#lastIndexOf のコード追加
  • 2016/03/28 10:21 String#lastIndexOf のコードが追加できていなかったのを修正。jsperfが削除されていたので比較検証用コードを作成してjsfiddleにUP
  • 2016/03/29 18:30 String#startsWith の Polyfill 追加。「実装」「結論」節の追加。

Re: re97 さん

投稿2016/03/26 12:46

編集2016/03/29 09:32
think49

総合スコア18164

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

mpyw

2016/03/26 14:31

今回は目的が「先頭文字を条件に応じて置換する」ことなので,一番シンプルにString#replaceで正規表現を使うのが適任かと思います.「^」による言明を行っておけば,String#indexOfのように極端に遅くなることは無いですし.
re97

2016/03/27 01:43

・回答内容難しかったです >比較するなら RegExp#test にしないと不公平な気はしますね ・使用すると、なぜ公平になるのでしょうか? >Polyfill を適用して汎用性の高い String#startWith を使うか ・String#startWithは標準で使用不可? ・先行実装しているライブラリを読み込めば、使用できるということでしょうか? ・「汎用性の高い」という意味は、String#startWithを使用すれば正規表現を使用しなくても先頭1文字一致を確認できる、という意味でしょうか?
think49

2016/03/27 03:06 編集

To: CertaiN さん 削除目的なら仰る通りだと思います。ところで、String#indexOf を String#lastIndexOf に置き換えれば走査速度の問題が解決される事に気が付きました。 To: re97 さん String#startsWith は IE11- で使用できません。Polyfillについては後程、追記します。 http://kangax.github.io/compat-table/es6/#test-String.prototype_methods_String.prototype.startsWith 汎用性も後で追記しますが、私は速度よりも「re97さんが求める要件にどれだけマッチしているか」が重要だと考えています。
mpyw

2016/03/27 07:06 編集

lastIndexOfの第2引数は有用ですね,勉強になりました.ところが… http://www.axlight.com/mt/sundayhacking/2013/03/javascriptstringstartswith.html ここに書かれているように,実際に自分で試してみてもlastIndexOfがV8においてはあまり速度が出ませんでした…安定してindexOfのほうが速かったです.(SpiderMonkeyだと予想通りな感じになったんですが,V8の最適化がかなり特殊なようです…)
think49

2016/03/28 03:35

Re: CertaiN さん 私の環境ではマッチしない場合に lastIndexOf が速い結果となりました(親記事参照)。 マッチする場合においては同等速度となり、これは期待通りの結果ですね。 indexOf はマッチする場合とマッチしない場合で速度差がない為、総合的には lastIndexOf が速いと判断出来る結果となりました。 Re: re97 さん 比較検証コードの修正まで出来ましたが、他についてはもう少し時間がかかりそうです。すみません。
think49

2016/03/29 09:37

遅くなりましたが、親記事を修正しました。 startsWith の Polyfill について。私の環境(GC49)では lastIndexOf がなかなかの速さだったので lastindexOf を採用しました。 下記コードのような while で1文字ずつ照合する組み方も個人的には嫌いではないので迷いましたがコーディング時間優先で lastIndexOf 採用に。 https://github.com/mathiasbynens/String.prototype.startsWith/blob/master/startswith.js lastIndexOf の仕様を読み進められていないのでもう少しコード量を減らせるような気はしています。
re97

2016/03/29 11:11

・回答&コメント&比較検証コードを作成いただき、ありがとうございました ・割と軽い気持ちで質問したのですが、自分が想定していた以上に色々なやり方があることを知り、かなり驚いています ・大変参考になりました
guest

0

こんなコードで試してみましたよ。

var arr=['※あいう', 'かきく']; var time; time = (new Date().valueOf()); for(var i = 0; i < 10000000; i++){ var str = arr[i%2]; var v = str.match(/^※.*/i); } console.log('regexp:' + (new Date().valueOf() - time)); time = (new Date().valueOf()); for(var i = 0; i < 10000000; i++){ var str = arr[i%2]; var v = str.indexOf('※') == 0; } console.log('indexOf:' + (new Date().valueOf() - time)); time = (new Date().valueOf()); for(var i = 0; i < 10000000; i++){ var str = arr[i%2]; var v = str.startsWith('※'); } console.log('startsWith:' + (new Date().valueOf() - time));

結果:
1回目
regexp:640
indexOf:701
startsWith:252

2回目
regexp:643
indexOf:678
startsWith:239

3回目
regexp:651
indexOf:671
startsWith:237

先頭文字限定ならstartsWithが速いみたい。

投稿2016/03/26 02:05

tkturbo

総合スコア5572

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

tkturbo

2016/03/26 02:10

charAtを忘れてたので、こっちでも試してみましたが、regexpやindexOfよりも輪をかけて遅かったのでびっくり。いあ!いあ!
mpyw

2016/03/26 11:22

「.*」のマッチは無駄だと思います.
mpyw

2016/03/26 11:27

何故か私の環境でstartsWithが異様に遅いんですがこれは何故だろう…
mpyw

2016/03/26 11:40

V8(Node.js, Chrome)とSpiderMonkey(Firefox)で全然結果が違うようです…
re97

2016/03/27 02:03 編集

・回答ありがとうございました ・indexOf や startsWith を初めて知りました ・色々なやり方があるんですね ・Chromeで試したら、startsWithが一番遅かったです ・環境によってこんなにも変わるんですね ・びっくりしました ・どういう風に評価したら良いか分からないです ・Chrome バージョン 49.0.2623.87 m ・regexp:707 ・indexOf:660 ・startsWith:1284
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問