Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

文字列系アルゴリズムのオーバーロードについて #26

Closed
terry-u16 opened this issue Sep 8, 2020 · 14 comments
Closed

Comments

@terry-u16
Copy link
Contributor

文字列系アルゴリズム(LCPArray, SuffixArray, ZAlgorithm)の引数ですが、ReadOnlySpan<T>を受け取るオーバーロードもあると良いのかなと思いました。
以下のようなイメージです。

定義側

public static partial class String
{
    public static int[] ZAlgorithm(string s) => ZAlgorithm<char>(s);
    public static int[] ZAlgorithm(int[] s) => ZAlgorithm<int>(s);
    public static int[] ZAlgorithm(uint[] s) => ZAlgorithm<uint>(s);
    public static int[] ZAlgorithm(long[] s) => ZAlgorithm<long>(s);
    public static int[] ZAlgorithm(ulong[] s) => ZAlgorithm<ulong>(s);

    public static int[] ZAlgorithm<T>(ReadOnlySpan<T> s) where T : IEquatable<T>
    {
        // Insert code here
    }
}

使用側

var a = new int[] { 1, 2, 3, 4, 5 };
var b = "hoge";
var za = String.ZAlgorithm(a);
var zb = String.ZAlgorithm(b);
var zc = String.ZAlgorithm(b.AsSpan(1, 2));

ReadOnlySpan<T>を受け付けるものさえあれば他はいらないのでは?とも思いましたが、型変換が入ると型推論が効かない(int[]ReadOnlySpan<int>と解釈してくれない)ようで、毎回型引数を明示しないといけないのが面倒そうなので諦めました……。

@key-moon
Copy link
Contributor

key-moon commented Sep 8, 2020

IEquatableのオーバーヘッドが少し気になりますが、ゼロコストでできるならば嬉しいですね。型変換についてはArrayを用いれば良さそうに思えますが、どうでしょうか…?

@terry-u16
Copy link
Contributor Author

ArrayではなくわざわざReadOnlySpan<T>を用いる動機としては、例えばABC141 E Who Says a Pun?のように部分文字列に対して文字列アルゴリズムを適用する場合、Substring()だと文字列のコピーが発生してしまう一方、AsSpan()ならコピーが発生しないため高速化できて嬉しいな……というものです。

IEquatable<T>のオーバーヘッドについては私も気になっていたため、以下のようなコードでベンチマークを取ってみました。

using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

class Program
{
    static void Main(string[] args)
    {
        BenchmarkRunner.Run<EquatableTest>();
    }
}

public class EquatableTest
{
    private int N = 20000;
    private int[] _data;

    [GlobalSetup]
    public void SetUp()
    {
        var random = new Random();
        _data = new int[N];
        for (int i = 0; i < _data.Length; i++)
        {
            _data[i] = random.Next();
        }
    }

    [Benchmark]
    public bool ArrayTest() => ArrayTest(_data);

    private bool ArrayTest(int[] a)
    {
        var result = false;     // 最適化によりループが消えるのを防ぐ
        for (int i = 0; i < a.Length; i++)
        {
            for (int j = 0; j < a.Length; j++)
            {
                result ^= a[i] == a[j];
            }
        }
        return result;
    }

    [Benchmark]
    public bool ReadOnlySpanTest() => ReadOnlySpanTest<int>(_data);

    private bool ReadOnlySpanTest<T>(ReadOnlySpan<T> a) where T : IEquatable<T>
    {
        var result = false;
        for (int i = 0; i < a.Length; i++)
        {
            for (int j = 0; j < a.Length; j++)
            {
                result ^= a[i].Equals(a[j]);
            }
        }
        return result;
    }
}

結果

// * Summary *

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.1016 (1909/November2018Update/19H2)
Intel Core i7-8700K CPU 3.70GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.1.300
  [Host]     : .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT
  DefaultJob : .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT


|           Method |     Mean |   Error |  StdDev |
|----------------- |---------:|--------:|--------:|
|        ArrayTest | 258.4 ms | 5.10 ms | 8.23 ms |
| ReadOnlySpanTest | 258.6 ms | 5.12 ms | 6.65 ms |

今回試した限りは誤差の範囲のようなので、おそらく大丈夫そうですね。

@kzrnm
Copy link
Owner

kzrnm commented Sep 8, 2020

IEquatable<T>をつけずEqualityComparer<T>.Defaultでも良いのではないでしょうか?

// * Summary *

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.1049 (1909/November2018Update/19H2)
Intel Core i7-4790 CPU 3.60GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.401
  [Host]     : .NET Core 3.1.7 (CoreCLR 4.700.20.36602, CoreFX 4.700.20.37001), X64 RyuJIT
  DefaultJob : .NET Core 3.1.7 (CoreCLR 4.700.20.36602, CoreFX 4.700.20.37001), X64 RyuJIT


|                           Method |     Mean |   Error |   StdDev |   Median |
|--------------------------------- |---------:|--------:|---------:|---------:|
|                        ArrayTest | 283.5 ms | 1.42 ms |  1.18 ms | 283.4 ms |
|                 ReadOnlySpanTest | 297.7 ms | 5.92 ms | 13.12 ms | 290.2 ms |
| ReadOnlySpanEqualityComparerTest | 288.3 ms | 4.66 ms |  4.13 ms | 286.6 ms |
    [Benchmark]
    public bool ReadOnlySpanEqualityComparerTest() => ReadOnlySpanEqualityComparerTest<int>(_data);

    private bool ReadOnlySpanEqualityComparerTest<T>(ReadOnlySpan<T> a)
    {
        var result = false;
        for (int i = 0; i < a.Length; i++)
        {
            for (int j = 0; j < a.Length; j++)
            {
                result ^= System.Collections.Generic.EqualityComparer<T>.Default.Equals(a[i], a[j]);
            }
        }
        return result;
    }

@terry-u16
Copy link
Contributor Author

ありがとうございます!
型制約外せて速度が同じであればそちらでもよさそうですね。

@key-moon
Copy link
Contributor

key-moon commented Sep 9, 2020

検証助かります…!

現状の文字列系アルゴリズムでは、SuffixArray 以外では等価判定が要件になってますね。
SA のみ比較が必要になってしまっていますがソート時だけなので、どちらにせよ Comparer を内部的に呼ぶので関係ないですね。オーバーヘッドなしで簡潔に実装できるこの方針がよさそうに感じます。(オーバーヘッドがないことは、JIT時にかかる特殊化で保証されているようです。(資料)

Comparer を用いた同様の比較の場合でもベンチを取ってみました。ソース

比較

Method Mean Error StdDev
Comparer 768.8 ms 15.25 ms 18.73 ms
GenericComparer 746.2 ms 14.52 ms 13.58 ms
CompareTo 687.8 ms 14.38 ms 33.04 ms
Operator 536.0 ms 10.56 ms 17.93 ms

等価判定

Method Mean Error StdDev Median
Comparer 140.1 ms 4.86 ms 13.70 ms 135.9 ms
GenericComparer 132.9 ms 2.66 ms 6.52 ms 132.8 ms
Equals 139.7 ms 2.76 ms 6.17 ms 139.7 ms
Operator 140.1 ms 2.79 ms 6.18 ms 141.4 ms

@key-moon
Copy link
Contributor

key-moon commented Sep 9, 2020

Spanのソートが追加される予定が .NET 5 からなので、現行だとソートを用いる SuffixArray を効率的に実装する方法が無いですね…。
dotnet/runtime#19969
dotnet/coreclr#27700

@terry-u16
Copy link
Contributor Author

細かく元コードの実装を追えてはいないので適当なことを言っていたら申し訳ないのですが……。
C++版をざっと見た限りではソート対象は内部で新たに確保したvectorのみで、入力文字列には触っていないように見えるので大丈夫そうな気がします?

@key-moon
Copy link
Contributor

Array.Sort(Array, Array)と同等の、keyとなるSpanを指定して他のSpanをソートするメソッドがあったので使いたいなというモチベーションでしたが、言われてみればそうでした。(目的を見失ってました)
比較関数をキャプチャありにした場合のコストが少々気になるので、手が空き次第実験してみようと思います。

@terry-u16
Copy link
Contributor Author

今更で申し訳ないのですが、ReadOnlySpan<T>はローカル関数等でキャプチャできないため、SuffixArrayのみ引数をReadOnlyMemory<T>に変更したいです。 #34

@kzrnm
Copy link
Owner

kzrnm commented Sep 10, 2020

ReadOnlySpan<T>を受け付けるものさえあれば他はいらないのでは?とも思いましたが、型変換が入ると型推論が効かない(int[]ReadOnlySpan<int>と解釈してくれない)ようで、毎回型引数を明示しないといけないのが面倒そうなので諦めました……。

    public static int[] ZAlgorithm(string s) => ZAlgorithm<char>(s.AsSpan());
    public static int[] ZAlgorithm(T[] s) where T : IEquatable<T> => ZAlgorithm<T>((ReadOnlySpan<T>)s);
    public static int[] ZAlgorithm<T>(ReadOnlySpan<T> s) where T : IEquatable<T>

ではダメなのでしょうか

@terry-u16
Copy link
Contributor Author

おっしゃる通りですね……!どうして気付かなかったのか……。
そちらの方が簡潔で良さそうに思えます。

@terry-u16
Copy link
Contributor Author

terry-u16 commented Sep 10, 2020

Spanのソートが追加される予定が .NET 5 からなので、

.NET 5が出るまでSuffixArraySpan<T>系オーバーロードはprivateにしておくのもアリかもしれませんね。

公開する部分はあまり後からいじりたくないですし、Z-algorithm等と違って部分列に対してsuffix arrayが欲しい場面ってそうそうなさそうな気がしますし。

#34

@key-moon
Copy link
Contributor

そうですね。 #34 の変更は private に現状しておくと解決しそうですね。

@terry-u16
Copy link
Contributor Author

Openにしっぱなしですみません……。こちらCloseとさせて頂きます。
皆様どうもありがとうございました!

kzrnm added a commit that referenced this issue Feb 27, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants