アンセケターメンはてなエディション

いつどこで誰に見られても恥ずかしいコードが置かれています

C# / .NET でひらがな判定・カタカナ変換&パフォーマンス比較

冬休みの自由研究として、C#/.NETでひらがな判定・ひらがな→カタカナ変換などをやってみました。

スマホでよみがなが入力されたらインクリメンタルサーチを実行するという動作を行うのに、入力された文字がすべてひらがなかどうかの判定が必要だったというのが動機です。具体的にはコミケWebカタログのアプリ連携でサクッとサークル検索したいなーというやつ。

変換方法がいくつかあったので、BenchmarkDotNetを使ってパフォーマンス比較をしてみました。

ソースコードはここに置いてあります。

https://github.com/anseketamen/KanaConvertStudy

1. ひらがな判定

C#/.NETでは文字の表現にUTF-16を使っていて、ひらがなは大体U+3041(ぁ)~U+3094(ゔ)にあります。あと長音(U+30FC)、読点(U+3001)、句点(U+3002)などもありますが、ひらがな判定に含めるかはケースバイケースで。

private static bool IsHiragana(char letter) 
    => (letter >= 0x3041 && letter <= 0x3094) || letter == 0x30FC;

詳しい表はWikipediaにあるのでそちらをどうぞ。ゖとかヺとか使いどころのわからない文字も並んでいて、見てるだけで楽しいです。

https://ja.wikipedia.org/wiki/Unicode%E4%B8%80%E8%A6%A7_3000-3FFF

1.1 正規表現で判定

textがすべてひらがなかどうか、こんな感じで判定できます。どっかからコピペしてきたので合ってるかは知りませんが、それっぽく動作はします。何文字だろうとforとかに頼らないのはステキ。

using System.Text.RegularExpressions;
var hiraganaRegex = new Regex("^([ぁ-ゔ]|ー)*$");

return hiraganaRegex.IsMatch(text);

1.2 forで回す

さて、ここからはさっきのひらがな判定のコードをforなどでぶんまわす実装です。

とりあえず愚直にforで回してみます。forの条件をLengthでアクセスすることで、ループごとの境界値チェックが省かれてちょっと早くなるとかなんとか。

for (var i = 0; i < text.Length; i++)
{
    if (!IsHiragana(text[i]))
    {
        return false;
    }
}
return true;

1.3 foreachで回す

foreachでも回してみます。こういうのだとコードの最適化で上のforと同一コードになるらしいです。

foreach (var t in text)
{
    if (!IsHiragana(t))
    {
        return false;
    }
}
return true;

1.4 LINQで回す

LINQでもやってみます。このシンプルさが最高。

return text.All(x => IsHiragana(x));

1.5 for(unsafe)で回す

unsafeコードを使ってstringを強制的にchar[]にしてforで回してみます。

fixed (char* p = text)
{
    for (var i = 0; i < text.Length; i++)
    {
        if (!IsHiragana(p[i]))
        {
            return false;
        }
    }
}
return true;

1.6 速度比較まとめ

Method Mean Error StdDev Gen 0 Allocated
正規表現 1,198.05 ns 40.564 ns 2.223 ns - -
for 37.91 ns 0.399 ns 0.022 ns - -
foreach 36.37 ns 0.760 ns 0.042 ns - -
LINQ 175.60 ns 7.325 ns 0.402 ns 0.0229 96 B
unsafe for 39.84 ns 2.449 ns 0.134 ns - -

項目の意味は以下です。英語力と.NET力が不足しているので正しいかは知りません。

  • Mean:実行時間の平均値
  • Error:99.9%信頼区間のプラマイ絶対値
  • StdDev:標準偏差
  • Gen 0:1000回あたりにガベージコレクタが走った回数
  • Allocated:確保されたメモリ

実行時間は for、foreach、unsafe for < LINQ <<< 正規表現 です。

for、foreach、unsafe forはほぼ拮抗しています。unsafe forはLengthアクセスが悪さしてるのかそれ以外の何かなのかわかりませんが、誤差レベルでちょっと遅いです。

そして、LINQだけアロケーションが発生しています。本来は正規表現アロケーション発生していますが、hiraganaRegexをreadonly staticなフィールドで保持して使いまわしているのでメソッドを呼ぶだけではnewされず、0 Byteとなっています。

まあ無難にforeachで回すのがよいでしょう。

2. ひらがな→カタカナ変換

上の方のWikipediaUnicode一覧表を見ればわかりますが、ひらがなに0x60を足すとカタカナになります。わーお簡単。

注意する点として、上で書いたひらがな判定のように長音もひらがなと判定していると、長音のカタカナ変換をしようとしておかしくなります。よってU+3041(ぁ)~U+3094(ゔ)のみをカタカナ変換するようにします。

ちなみに、なぜひらがな→カタカナ変換を調べることになったかというと、Circle.msから送られてくるよみがなデータがカタカナっぽいからです。変換しないとインクリメンタルサーチできないのです。

2.1 VB.NETのStrConv関数を使う

Windowsでしか使えないため.NET MAUIでスマホアプリを作るような場合には不向きですが、一応比較のためにやってみます。

//Shift_JIS関連のおまじない
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);

return Strings.StrConv(text, VbStrConv.Katakana, 0x411);

さて、残りはforなどでぶん回す実装です。

2.2 forで回す

var textArr = text.ToCharArray();
for (var i = 0; i < textArr .Length; i++)
{
    if (IsHiragana(textArr[i]))
    {
        text[i] = (char)(textArr[i] + 0x60);
    }
}
return new string(textArr);

2.3 LINQで回す

サクッと書けるやつ。new stringがダサすぎるのでchar[]IEnumerable<char>の拡張メソッドを生やしたくなります。

return new string(text
    .Select(x => IsHiragana(x) ? (char)(x + 0x60) : x)
    .ToArray());

2.4 for(unsafe)で回す

forやLINQの実装を見てるとToCharArrayとかnew stringとかアロケーションが発生してそうで嫌ですよね。そんなあなたにunsafe。

fixed (char* p = text)
{
    for (var i = 0; i < text.Length; i++)
    {
        if (IsHiragana(p[i]))
        {
            p[i] = (char)(p[i] + 0x60);
        }
    }
}
return text;

2.5 速度比較まとめ

Method Mean Error StdDev Gen 0 Allocated
VB.NET StrConv 3,473.99 ns 1,011.636 ns 55.451 ns 0.3853 1,616 B
for 182.38 ns 51.919 ns 2.846 ns 0.0610 256 B
LINQ 820.91 ns 189.860 ns 10.407 ns 0.1659 696 B
unsafe for 80.56 ns 9.147 ns 0.501 ns - -

実行時間は unsafe for < for < LINQ << VB.NET です。

VB.NETはだめですね。Shift_JISのおまじないも必要だしWindowsでしか動かないしパフォーマンスも最低です。

LINQ、for、unsafe forの順にコードが汚くなり、パフォーマンスが向上しています。

サクッと書くならLINQで、パフォーマンス重視ならunsafe forがいいんじゃないかなと思います。forで書くくらいならunsafe forにしたほうがよさそう。

3. 結論

ひらがな判定はforeachで文字ごとに比較し、ひらがな→カタカナ判定はunsafeを使ってごにょごにょするのがよいのではないかという結論になりました。

ひらがな判定本体(IsHiragana)に関しては、短絡評価を考慮して、入力されるであろう文字列(アルファベットなのかカタカナなのか漢字なのか)によって不等式の向きや順番を変えてあげると実用上のパフォーマンスが若干変わってくるかもしれません。めんどくさいのでここではしませんが。