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

C#に存在しない機能を用いた実装について #20

Closed
key-moon opened this issue Sep 7, 2020 · 8 comments
Closed

C#に存在しない機能を用いた実装について #20

key-moon opened this issue Sep 7, 2020 · 8 comments

Comments

@key-moon
Copy link
Contributor

key-moon commented Sep 7, 2020

存在しない機能を用いた実装について、どう対応するかをまとめたい

今現在問題になりそうなもの

  • 定数テンプレート(Modint/SegTree/LazySegTree)
  • 整数テンプレート(MinCostFlow/MaxFlow...)
  • 演算子の柔軟な解決(FenwickTree)
  • 一括include(<atcoder/all>)
  • type_traits
@key-moon
Copy link
Contributor Author

key-moon commented Sep 7, 2020

一括includeは AtCoder.Modint みたいなNamespace構成にするなら問題になりそうですが、C#標準ではしてないので問題にならなさそうですね。
AtCoderの中にぶちまけてしまうか、AtCoder.DataStructure/AtCoder.Math/AtCoder.Graph のように分けるかで問題にはなりそうです。

@key-moon
Copy link
Contributor Author

key-moon commented Sep 7, 2020

Convolution/Modint/MaxFlow/MinCostFlowのインターフェイス仮決定は整数テンプレートの影響で見送ります。 #11 #12 #14 #15

@key-moon
Copy link
Contributor Author

key-moon commented Sep 7, 2020

FenwickTree/SegTree/LazySegTreeのインターフェイス仮決定は定数テンプレートと型引数の演算子の解決の影響で見送ります。 #1 #2 #3

@terry-u16
Copy link
Contributor

整数・実数テンプレート、いくつか手段があると思っていて、

  1. とりあえずlongで書いてしまって、必要に応じてその場その場で手で置換
  2. 式木を使う
  3. dynamicを使う(見るからに遅そう)
  4. 四則演算メソッドを持つインターフェースを用意する

あたりがパッと思い付くものかなと考えています。
(リフレクションとかも考えられますが割愛。他に何かあれば教えてください!)

個人的には少々面倒ながら4.あたりかなと考えていたのですが、実行速度が気になるところなので、長いですが以下のコードでそれぞれ速度比較を行ってみました。

using System;
using System.Linq.Expressions;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

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

public class GenericAddTest
{
    const int N = 1_000_000_000;

    /// <summary>
    /// 1. 生longで足し算
    /// </summary>
    [Benchmark]
    public long LongAdd() => LongAdd(1L);

    private long LongAdd(long init)
    {
        long x = init;
        for (int i = 0; i < N; i++)
        {
            x = x + x;
        }
        return x;
    }

    /// <summary>
    /// 2. 式木で足し算
    /// </summary>
    [Benchmark]
    public long ExpressionAdd() => ExpressionAdd(1L);

    private T ExpressionAdd<T>(T init)
    {
        var p1 = Expression.Parameter(typeof(T));
        var p2 = Expression.Parameter(typeof(T));
        var add = Expression.Lambda<Func<T, T, T>>(Expression.Add(p1, p2), p1, p2).Compile();

        T x = init;
        for (int i = 0; i < N; i++)
        {
            x = add(x, x);
        }
        return x;
    }

    /// <summary>
    /// 3. dynamicで足し算
    /// </summary>
    [Benchmark]
    public long DynamicAdd() => (long)DynamicAdd(1L);

    private dynamic DynamicAdd(dynamic init)
    {
        dynamic x = init;
        for (int i = 0; i < N; i++)
        {
            x = x + x;
        }
        return x;
    }

    /// <summary>
    /// 4. structにインターフェースを実装し、メンバメソッドを呼び出す
    /// </summary>
    [Benchmark]
    public long GenericStructAdd() => GenericAdd(new LongStruct(1L)).Value;

    private T GenericAdd<T>(T init) where T : IAddable<T>
    {
        T x = init;
        for (int i = 0; i < N; i++)
        {
            x = x.Add(x);
        }
        return x;
    }

    /// <summary>
    /// 5. structにインターフェースを実装し、オーバーロードされた演算子を呼び出す
    /// </summary>
    [Benchmark]
    public long GenericStructAddWithOperator() => GenericAddWithOperator(new LongStruct(1L)).Value;

    private T GenericAddWithOperator<T>(T init) where T : IAddable<T>
    {
        T x = init;
        for (int i = 0; i < N; i++)
        {
            x = x + x;  // Boxing!!!
        }
        return x;
    }

    /// <summary>
    /// 6. classにインターフェースを実装し、メンバメソッドを呼び出す
    /// </summary>
    [Benchmark]
    public long GenericClassAdd() => GenericAdd(new LongClass(1L)).Value;

    interface IAddable<T> where T : IAddable<T>
    {
        public T Add(T other);
        public static T operator +(IAddable<T> x, T y) => x.Add(y);
    }

    readonly struct LongStruct : IAddable<LongStruct>
    {
        public long Value { get; }

        public LongStruct(long value) => Value = value;
        public LongStruct Add(LongStruct other) => new LongStruct(Value + other.Value);
        public static implicit operator long(LongStruct x) => x.Value;
    }

    class LongClass : IAddable<LongClass>
    {
        public long Value { get; }

        public LongClass(long value) => Value = value;
        public LongClass Add(LongClass other) => new LongClass(Value + other.Value);
        public static implicit operator long(LongClass x) => x.Value;
    }
}

実行結果

Method Mean Error StdDev
LongAdd 234.4 ms 1.33 ms 1.04 ms
ExpressionAdd 1,403.1 ms 6.15 ms 5.75 ms
DynamicAdd 5,539.6 ms 35.84 ms 31.77 ms
GenericStructAdd 233.9 ms 0.66 ms 0.58 ms
GenericStructAddWithOperator 4,392.4 ms 86.21 ms 80.64 ms
GenericClassAdd 4,636.4 ms 70.74 ms 59.07 ms

上から順番に、

  1. longで足し算
  2. 式木で足し算
  3. dynamicで足し算
  4. structにインターフェースを実装し、メンバメソッドを呼び出す
  5. structにインターフェースを実装し、オーバーロードされた演算子を呼び出す
  6. classにインターフェースを実装し、メンバメソッドを呼び出す

の実行時間です。

2.の式木は生longの5~6倍、3.のdynamicは20倍くらい差が付いています。4.はジェネリックがコード展開されるおかげか、生のlongと同等の速度が出ていますね。一方で5.はインターフェースを経由することでボックス化が生じるせいか、相当遅くなってしまうようです……。6.のclassは毎回ヒープ確保が生じるため、これもやっぱり遅くなってしまいますね。

判断の一助となれば嬉しいです。ツッコミ所等もあれば教えてください……!

@takytank
Copy link
Contributor

takytank commented Sep 9, 2020

実際に最大流のライブラリで試してみたのがこちらです。
https://atcoder.jp/contests/practice2/submissions/16600924
https://atcoder.jp/contests/practice2/submissions/16601991

↓抜粋

public interface ICap<TValue, TCap> : IEquatable<TCap>
	 where TValue : struct, IEquatable<TValue>, IComparable<TValue>
	 where TCap : ICap<TValue, TCap>, IEquatable<TCap>, new()
{
	public static TCap Zero => new TCap();
	public static TCap Limit => new TCap().GetLimit();
	TValue Value { get; set; }
	bool IsZero();
	TCap GetLimit();
	TCap Add(ICap<TValue, TCap> value);
	TCap Sub(ICap<TValue, TCap> value);
	public static TCap operator +(ICap<TValue, TCap> lhs, ICap<TValue, TCap> rhs)
		=> lhs.Add(rhs);
	public static TCap operator -(ICap<TValue, TCap> lhs, ICap<TValue, TCap> rhs)
		=> lhs.Sub(rhs);
	public static bool operator <(ICap<TValue, TCap> lhs, ICap<TValue, TCap> rhs)
		=> lhs.Value.CompareTo(rhs.Value) < 0;
	public static bool operator >(ICap<TValue, TCap> lhs, ICap<TValue, TCap> rhs)
		=> lhs.Value.CompareTo(rhs.Value) > 0;
}

public struct CapInt : ICap<int, CapInt>
{
	public int Value { get; set; }
	public CapInt(int value) => Value = value;
	public bool IsZero() => Value == 0;
	public CapInt GetLimit() => new CapInt(int.MaxValue);
	public CapInt Add(ICap<int, CapInt> value) => new CapInt { Value = this.Value + value.Value };
	public CapInt Sub(ICap<int, CapInt> value) => new CapInt { Value = this.Value - value.Value };
	public bool Equals(CapInt other) => Value == other.Value;
}

public struct CapLong : ICap<long, CapLong>
{
	public long Value { get; set; }
	public CapLong(long value) => Value = value;
	public bool IsZero() => Value == 0;
	public CapLong GetLimit() => new CapLong(long.MaxValue);
	public CapLong Add(ICap<long, CapLong> value) => new CapLong { Value = this.Value + value.Value };
	public CapLong Sub(ICap<long, CapLong> value) => new CapLong { Value = this.Value - value.Value };
	public bool Equals(CapLong other) => Value == other.Value;
}

public class MFGraph<TValue, TCap>
	where TValue : struct, IEquatable<TValue>, IComparable<TValue>
	where TCap : ICap<TValue, TCap>, IEquatable<TCap>, new()
{
}

おそらくGenericStructAddWithOperatorに相当するものだと思うのですが、思っていたよりかは遅くなかったです。(むしろこのALPC-Dの結果だけを見る分には速いです)
ですが、多少無理矢理なところもありますし、あまりおすすめできないかなと……

C++側も、intとlong longのみを想定している様ですし、とりあえずlong限定で書いてライブラリを使えるようにするのがいいのでは?と個人的には思います。

@key-moon
Copy link
Contributor Author

以下のコードでそれぞれ速度比較を行ってみました。

ありがとうございます…!
struct の自動展開がかなり偉そうですね。関数テンプレートを用いているものや演算子を云々しているものはこれで実装するのが良いと感じます。

とりあえずlong限定で書いてライブラリを使えるようにするのがいいのでは?

ユーザーに公開するインターフェイスには見えないようにするのが賢明かもしれないとは思いつつ、インターフェイスを変更するのはどうかという思いもあるので今は判断しきれないですね…

@kzrnm
Copy link
Owner

kzrnm commented Sep 10, 2020

IComparable<T>IComparer<T>があるように、IAddable<T>に対してIAdder<T>で試してみました。
structの場合はインライン展開されるためか直接操作と同等のスピードが出ます。
Addableと違って変換不要というメリットもあります。

Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
Direct 1.975 s 0.0161 s 0.0151 s - - - 24 B
Addable 2.065 s 0.0071 s 0.0067 s - - - 1264 B
AdderClass 3.616 s 0.0205 s 0.0191 s - - - 56 B
AdderStruct 1.928 s 0.0125 s 0.0117 s - - - 32 B
Raw
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.1082 (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 | Gen 0 | Gen 1 | Gen 2 | Allocated |
|------------ |--------:|---------:|---------:|------:|------:|------:|----------:|
|      Direct | 1.975 s | 0.0161 s | 0.0151 s |     - |     - |     - |      24 B |
|     Addable | 2.065 s | 0.0071 s | 0.0067 s |     - |     - |     - |    1264 B |
|  AdderClass | 3.616 s | 0.0205 s | 0.0191 s |     - |     - |     - |      56 B |
| AdderStruct | 1.928 s | 0.0125 s | 0.0117 s |     - |     - |     - |      32 B |
using System;
using System.Linq;
using System.Linq.Expressions;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Running;

class Program
{
    static void Main()
    {
        BenchmarkRunner.Run<AddBench>(DefaultConfig.Instance.AddDiagnoser(MemoryDiagnoser.Default));
    }
}

public class AddBench
{
    const int N = 1000_000_000;
    const long sumN = 499999999500000000;

    [Benchmark]
    public long Direct()
    {
        var sum = new SumDirect();
        for (int i = 0; i < N; i++)
            sum.Add(i);
        var result = sum.Value;
        if (result != sumN) throw new Exception();
        return result;
    }

    [Benchmark]
    public long Addable()
    {
        var sum = new SumAddable<AddableLong>();
        for (int i = 0; i < N; i++)
            sum.Add(i);
        var result = sum.Addable.value;
        if (result != sumN) throw new Exception();
        return result;
    }

    [Benchmark]
    public long AdderClass()
    {
        var sum = new SumAdder<long, LongAdderClass>(new LongAdderClass());
        for (int i = 0; i < N; i++)
            sum.Add(i);
        var result = sum.Value;
        if (result != sumN) throw new Exception();
        return result;
    }

    [Benchmark]
    public long AdderStruct()
    {
        var sum = new SumAdder<long, LongAdderStruct>();
        for (int i = 0; i < N; i++)
            sum.Add(i);
        var result = sum.Value;
        if (result != sumN) throw new Exception();
        return result;
    }
}
public class SumDirect
{
    public long Value { private set; get; }
    public long Add(long val) => this.Value += val;
}
public interface IAddable<T> where T : IAddable<T>
{
    public T Add(T other);
}
public readonly struct AddableLong : IAddable<AddableLong>
{
    public AddableLong(long val) { this.value = val; }
    public readonly long value;
    public AddableLong Add(AddableLong other) => new AddableLong(value + other.value);
    public static implicit operator AddableLong(long val) => new AddableLong(val);
}
public class SumAddable<T> where T : IAddable<T>
{
    public T Addable { private set; get; }
    public T Add(T val) => Addable = Addable.Add(val);
}


public interface IAdder<T>
{
    public T Add(T v1, T v2);
}
public class LongAdderClass : IAdder<long>
{
    public long Add(long v1, long v2) => v1 + v2;
}
public struct LongAdderStruct : IAdder<long>
{
    public long Add(long v1, long v2) => v1 + v2;
}
public class SumAdder<T, A> where A : IAdder<T>
{
    private readonly A adder;
    public SumAdder() { }
    public SumAdder(A adder)
    {
        this.adder = adder;
    }
    public T Value { private set; get; }
    public T Add(T val) => Value = adder.Add(Value, val);
}

// このような取得用クラスを用意してしまうのもアリか。
public static class SumAdder
{
    public static SumAdder<long, LongAdderStruct> Long() => new SumAdder<long, LongAdderStruct>();
}

// mcf_graph<Cap, Cost>を実現するならこんな感じになる?
public static class McfGraph
{
    public static McfGraph<long, LongOperator, long, LongOperator> LongLong(int n) => new McfGraph<long, LongOperator, long, LongOperator>(n);
    public static McfGraph<int, IntOperator, long, LongOperator> IntLong(int n) => new McfGraph<int, IntOperator, long, LongOperator>(n);
    public static McfGraph<long, LongOperator, int, IntOperator> LongInt(int n) => new McfGraph<long, LongOperator, int, IntOperator>(n);
    public static McfGraph<int, IntOperator, int, IntOperator> IntInt(int n) => new McfGraph<int, IntOperator, int, IntOperator>(n);
}

@terry-u16 terry-u16 mentioned this issue Sep 12, 2020
kzrnm referenced this issue in FKbelm/ac-library-csharp-old Nov 12, 2020
kzrnm added a commit that referenced this issue Feb 27, 2022
BinarySearch targets only IComparable<T>
@kzrnm
Copy link
Owner

kzrnm commented Feb 27, 2022

実装済み

@kzrnm kzrnm closed this as completed 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

4 participants