Иногда рефлексивные вызовы дороги в терминах производительности и не могут быть опущены.

В этой статье представлены паттерны, позволяющие существенно повысить производительность множественных рефлексивных вызовов посредством техники виртуализации (кэширования) результатов.

image

Рассмотрим следующий метод:

static void AnyPerformanceCriticalMethod()
{
	var anyTypeName = typeof(AnyType).Name;
	/* ... using of anyTypeName ... */
}

Важный паттерн в подобных случаях — это кэширование локальной переменной в качестве статической, например:

/* much better! */
static readonly string AnyTypeName = typeof(AnyType).Name;

static void AnyPerformanceCriticalMethod()
{
	/* ... using of AnyTypeName ... */
}

Но как поступить в случае обобщённого (generic) метода?

static void AnyPerformanceCriticalMethod<T>()
{
	var anyTypeName = typeof(T).Name;
	/* ... using of anyTypeName ... */
}

Существует практичное обобщённое решение, взгляните.

TypeOf.cs
public static class TypeOf<T>
{
	/* Important! Should be a readonly variable for best performance */ 
	public static readonly Type Raw = typeof(T);
	
	public static readonly string Name = Raw.Name;
	public static readonly Assembly Assembly = Raw.Assembly;
	public static readonly bool IsValueType = Raw.IsValueType;
	/* etc. */
}

static void AnyPerformanceCriticalMethod<T>()
{
	var anyTypeName = TypeOf<T>.Name;
	/* ... using of anyTypeName ... */
}

*Примечательно, что до момента добавления обобщённых классов и методов, C# уже имел поддержку ряда обобщённых операторов: `typeof`, `is`, `as`

Что насчёт другого сценария?

static void AnyPerformanceCriticalMethod(object item)
{
	var itemTypeName = o.GetType().Name;
	/* ... using of anyTypeName ... */
}

Можем попробовать.

RipeType.cs
public class RipeType
{
	internal RipeType(Type raw)
	{
		Raw = raw;
		
		Name = raw.Name;
		Assembly = raw.Assembly;
		IsValueType = raw.IsValueType;
		/* etc. */
	}

	public static Type Raw { get; }
	
	public string Name { get; }
	public Assembly Assembly { get; }
	public bool IsValueType { get; }
	/* etc. */
}

TypeOf.cs
* Примечание: как указали в комментариях, для более надёжной потокобезопасности нужно использовать ConcurrentDictionary (это может повлиять на результаты тестов)
public static class TypeOf
{
	private static readonly object SyncRoot = new object();

	private static readonly Dictionary<Type, RipeType> RawToRipe = new Dictionary<Type, RipeType>();

	public static RipeType ToRipeType(this Type type) =>
		RawToRipe.TryGetValue(type, out var typeData)
			? typeData
			: Lock.Invoke(SyncRoot, () => RawToRipe.TryGetValue(type, out typeData)
				? typeData // may catch item created into a different thread
				: RawToRipe[type] = new RipeType(type));
				
	public static RipeType GetRipeType(this object o) => o.GetType().ToRipeType();
}

Lock.cs
public static class Lock
{
	public static void Invoke<TSyncContext>(TSyncContext customSyncContext, Action action)
	{
		lock (customSyncContext) action();
	}

	public static TResult Invoke<TSyncContext, TResult>(TSyncContext customSyncContext, Func<TResult> func)
	{
		lock (customSyncContext) return func();
	}
}

Итак, теперь можно использовать:

static void AnyPerformanceCriticalMethod(object item)
{
	var itemTypeName = o.GetRipeType().Name;
	/* ... using of anyTypeName ... */
}

Что насчёт недостатков `TypeOf` паттерна?
* `typeof(List<>)` допустимо
* `TypeOf<List<>>` не допустимо

Как решить?

var listAssemby = TypeOf.List.Assembly;

где

public static class TypeOf
{
	/* ... */

	public static readonly RipeType Object = typeof(object).ToRipeType();
	public static readonly RipeType String = typeof(string).ToRipeType();
	public static readonly RipeType Array = typeof(Array).ToRipeType();
	public static readonly RipeType Type = typeof(Type).ToRipeType();
	public static readonly RipeType List = typeof(List<>).ToRipeType();
	public static readonly RipeType IList = typeof(IList<>).ToRipeType();
	public static readonly RipeType Dictionary = typeof(Dictionary<,>).ToRipeType();
	public static readonly RipeType IDictionary = typeof(IDictionary<,>).ToRipeType();
	public static readonly RipeType KeyValuePair = typeof(KeyValuePair<,>).ToRipeType();
	public static readonly RipeType DictionaryEntry = typeof(DictionaryEntry).ToRipeType();
}

Самое время для бенчмарков!

typeof vs. TypeOf [BenchmarkDotNet - code]
[
	CoreJob,
	ClrJob,
	MonoJob("Mono", @"C:\Program Files\Mono\bin\mono.exe")
]
public class TypeOfBenchmarks
{
	[Benchmark] public Type typeof_int() => typeof(int);
	[Benchmark] public Type TypeOf_int() => TypeOf<int>.Raw;
	[Benchmark] public Type typeof_string() => typeof(string);
	[Benchmark] public Type TypeOf_string() => TypeOf<string>.Raw;

	[Benchmark] public string typeof_int_Name() => typeof(int).Name;
	[Benchmark] public string TypeOf_int_Name() => TypeOf<int>.Name;
	[Benchmark] public string typeof_string_Name() => typeof(string).Name;
	[Benchmark] public string TypeOf_string_Name() => TypeOf<string>.Name;
	
	[Benchmark] public Assembly typeof_int_Assembly() => typeof(int).Assembly;
	[Benchmark] public Assembly TypeOf_int_Assembly() => TypeOf<int>.Assembly;
	[Benchmark] public Assembly typeof_string_Assembly() => typeof(string).Assembly;
	[Benchmark] public Assembly TypeOf_string_Assembly() => TypeOf<string>.Assembly;
	
	[Benchmark] public bool typeof_int_IsValueType() => typeof(int).IsValueType;
	[Benchmark] public bool TypeOf_int_IsValueType() => TypeOf<int>.IsValueType;
	[Benchmark] public bool typeof_string_IsValueType() => typeof(string).IsValueType;
	[Benchmark] public bool TypeOf_string_IsValueType() => TypeOf<string>.IsValueType;
}


typeof vs. TypeOf [BenchmarkDotNet - results]
Total time: 00:23:34 (1414.47 sec)

// * Summary *

BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17134
Intel Core i7-3517U CPU 1.90GHz (Ivy Bridge), 1 CPU, 4 logical and 2 physical cores
Frequency=2338440 Hz, Resolution=427.6355 ns, Timer=TSC
.NET Core SDK=2.1.302
  [Host] : .NET Core 2.1.2 (CoreCLR 4.6.26628.05, CoreFX 4.6.26629.01), 64bit RyuJIT
  Clr    : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3160.0
  Core   : .NET Core 2.1.2 (CoreCLR 4.6.26628.05, CoreFX 4.6.26629.01), 64bit RyuJIT
  Mono   : Mono 5.12.0 (Visual Studio), 64bit 


                    Method |  Job | Runtime |        Mean |      Error |     StdDev |
-------------------------- |----- |-------- |------------:|-----------:|-----------:|
                typeof_int |  Clr |     Clr |   3.2686 ns |  0.0490 ns |  0.0434 ns |
                TypeOf_int |  Clr |     Clr |   0.0495 ns |  0.1124 ns |  0.0939 ns |
             typeof_string |  Clr |     Clr |   3.1980 ns |  0.0288 ns |  0.0270 ns |
             TypeOf_string |  Clr |     Clr |   0.0520 ns |  0.0773 ns |  0.0723 ns |
           typeof_int_Name |  Clr |     Clr |  19.4201 ns |  0.1220 ns |  0.1141 ns |
           TypeOf_int_Name |  Clr |     Clr |   0.0082 ns |  0.0169 ns |  0.0159 ns |
        typeof_string_Name |  Clr |     Clr |  19.5041 ns |  0.1397 ns |  0.1090 ns |
        TypeOf_string_Name |  Clr |     Clr |   0.0007 ns |  0.0031 ns |  0.0028 ns |
       typeof_int_Assembly |  Clr |     Clr |  33.8565 ns |  0.6931 ns |  0.5788 ns |
       TypeOf_int_Assembly |  Clr |     Clr |   0.0034 ns |  0.0130 ns |  0.0115 ns |
    typeof_string_Assembly |  Clr |     Clr |  33.9922 ns |  0.2244 ns |  0.1989 ns |
    TypeOf_string_Assembly |  Clr |     Clr |   0.0001 ns |  0.0004 ns |  0.0003 ns |
    typeof_int_IsValueType |  Clr |     Clr |  56.1685 ns |  0.3858 ns |  0.3420 ns |
    TypeOf_int_IsValueType |  Clr |     Clr |   0.4990 ns |  0.0141 ns |  0.0132 ns |
 typeof_string_IsValueType |  Clr |     Clr |  94.0358 ns |  0.4386 ns |  0.3662 ns |
 TypeOf_string_IsValueType |  Clr |     Clr |   0.4960 ns |  0.0109 ns |  0.0102 ns |

                typeof_int | Core |    Core |   1.9114 ns |  0.0527 ns |  0.0493 ns |
                TypeOf_int | Core |    Core |   6.1310 ns |  0.0494 ns |  0.0462 ns |
             typeof_string | Core |    Core |   2.2120 ns |  0.0522 ns |  0.0436 ns |
             TypeOf_string | Core |    Core |   6.1174 ns |  0.0481 ns |  0.0401 ns |
           typeof_int_Name | Core |    Core |  19.5100 ns |  0.1998 ns |  0.1771 ns |
           TypeOf_int_Name | Core |    Core |   6.1495 ns |  0.0829 ns |  0.0735 ns |
        typeof_string_Name | Core |    Core |  19.3662 ns |  0.0895 ns |  0.0793 ns |
        TypeOf_string_Name | Core |    Core |   6.1589 ns |  0.0314 ns |  0.0278 ns |
       typeof_int_Assembly | Core |    Core |  23.4876 ns |  0.1885 ns |  0.1763 ns |
       TypeOf_int_Assembly | Core |    Core |   6.1362 ns |  0.0415 ns |  0.0388 ns |
    typeof_string_Assembly | Core |    Core |  25.5613 ns |  0.2293 ns |  0.2033 ns |
    TypeOf_string_Assembly | Core |    Core |   6.1082 ns |  0.0352 ns |  0.0312 ns |
    typeof_int_IsValueType | Core |    Core |  49.8048 ns |  0.2305 ns |  0.1925 ns |
    TypeOf_int_IsValueType | Core |    Core |   7.1171 ns |  0.0477 ns |  0.0423 ns |
 typeof_string_IsValueType | Core |    Core |  84.8155 ns |  0.7962 ns |  0.7058 ns |
 TypeOf_string_IsValueType | Core |    Core |   7.0987 ns |  0.0521 ns |  0.0487 ns |

                typeof_int | Mono |    Mono |   0.0725 ns |  0.0229 ns |  0.0214 ns |
                TypeOf_int | Mono |    Mono |   3.0123 ns |  0.0652 ns |  0.0610 ns |
             typeof_string | Mono |    Mono |   0.0185 ns |  0.0206 ns |  0.0193 ns |
             TypeOf_string | Mono |    Mono |   9.3828 ns |  0.0863 ns |  0.0765 ns |
           typeof_int_Name | Mono |    Mono | 429.8195 ns |  4.4049 ns |  3.6783 ns |
           TypeOf_int_Name | Mono |    Mono |   2.3856 ns |  0.1608 ns |  0.1426 ns |
        typeof_string_Name | Mono |    Mono | 439.3774 ns |  1.2985 ns |  1.2146 ns |
        TypeOf_string_Name | Mono |    Mono |   8.8580 ns |  0.0728 ns |  0.0646 ns |
       typeof_int_Assembly | Mono |    Mono | 223.5933 ns |  0.6152 ns |  0.5454 ns |
       TypeOf_int_Assembly | Mono |    Mono |   2.2587 ns |  0.0494 ns |  0.0462 ns |
    typeof_string_Assembly | Mono |    Mono | 227.3259 ns |  0.6448 ns |  0.5716 ns |
    TypeOf_string_Assembly | Mono |    Mono |   9.3276 ns |  0.1215 ns |  0.1136 ns |
    typeof_int_IsValueType | Mono |    Mono | 490.2376 ns |  4.3860 ns |  4.1027 ns |
    TypeOf_int_IsValueType | Mono |    Mono |   3.1849 ns |  0.0145 ns |  0.0129 ns |
 typeof_string_IsValueType | Mono |    Mono | 997.4254 ns | 11.6159 ns | 10.8655 ns |
 TypeOf_string_IsValueType | Mono |    Mono |   9.6504 ns |  0.0354 ns |  0.0331 ns |


Type vs. RipeType [BenchmarkDotNet - code]
[
	CoreJob,
	ClrJob,
	MonoJob("Mono", @"C:\Program Files\Mono\bin\mono.exe")
]
public class RipeTypeBenchmarks
{
	static object o = new object();
	readonly Type rawType = o.GetType();
	readonly RipeType ripeType = o.GetRipeType();
	
	[Benchmark] public string RawType_Name() => rawType.Name;
	[Benchmark] public string RipeType_Name() => ripeType.Name;
	[Benchmark] public string GetRawType_Name() => o.GetType().Name;
	[Benchmark] public string GetRipeType_Name() => o.GetRipeType().Name;

	[Benchmark] public Assembly RawType_Assembly() => rawType.Assembly;
	[Benchmark] public Assembly RipeType_Assembly() => ripeType.Assembly;
	[Benchmark] public Assembly GetRawType_Assembly() => o.GetType().Assembly;
	[Benchmark] public Assembly GetRipeType_Assembly() => o.GetRipeType().Assembly;

	[Benchmark] public bool RawType_IsValueType() => rawType.IsValueType;
	[Benchmark] public bool RipeType_IsValueType() => ripeType.IsValueType;
	[Benchmark] public bool GetRawType_IsValueType() => o.GetType().IsValueType;
	[Benchmark] public bool GetRipeType_IsValueType() => o.GetRipeType().IsValueType;
}


Type vs. RipeType [BenchmarkDotNet - results]
Total time: 00:14:59 (899.57 sec)

// * Summary *

BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17134
Intel Core i7-3517U CPU 1.90GHz (Ivy Bridge), 1 CPU, 4 logical and 2 physical cores
Frequency=2338440 Hz, Resolution=427.6355 ns, Timer=TSC
.NET Core SDK=2.1.302
  [Host] : .NET Core 2.1.2 (CoreCLR 4.6.26628.05, CoreFX 4.6.26629.01), 64bit RyuJIT
  Clr    : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3160.0
  Core   : .NET Core 2.1.2 (CoreCLR 4.6.26628.05, CoreFX 4.6.26629.01), 64bit RyuJIT
  Mono   : Mono 5.12.0 (Visual Studio), 64bit


                  Method |  Job | Runtime |        Mean |     Error |    StdDev |
------------------------ |----- |-------- |------------:|----------:|----------:|
            RawType_Name |  Clr |     Clr |  10.2733 ns | 0.1112 ns | 0.1040 ns |
           RipeType_Name |  Clr |     Clr |   0.0164 ns | 0.0220 ns | 0.0206 ns |
         GetRawType_Name |  Clr |     Clr |  15.3661 ns | 0.4064 ns | 0.7431 ns |
        GetRipeType_Name |  Clr |     Clr |  43.3530 ns | 0.4160 ns | 0.3474 ns |
        RawType_Assembly |  Clr |     Clr |  19.8898 ns | 0.1967 ns | 0.1840 ns |
       RipeType_Assembly |  Clr |     Clr |   0.0002 ns | 0.0010 ns | 0.0009 ns |
     GetRawType_Assembly |  Clr |     Clr |  22.7084 ns | 0.1512 ns | 0.1340 ns |
    GetRipeType_Assembly |  Clr |     Clr |  43.1685 ns | 0.3532 ns | 0.3304 ns |
     RawType_IsValueType |  Clr |     Clr |  35.7668 ns | 0.2840 ns | 0.2517 ns |
    RipeType_IsValueType |  Clr |     Clr |   0.0005 ns | 0.0020 ns | 0.0018 ns |
  GetRawType_IsValueType |  Clr |     Clr |  39.6176 ns | 0.2465 ns | 0.2306 ns |
 GetRipeType_IsValueType |  Clr |     Clr |  43.4645 ns | 0.9240 ns | 0.8643 ns |
 
            RawType_Name | Core |    Core |  10.7102 ns | 0.1705 ns | 0.1511 ns |
           RipeType_Name | Core |    Core |   0.0075 ns | 0.0154 ns | 0.0144 ns |
         GetRawType_Name | Core |    Core |  12.8294 ns | 0.0698 ns | 0.0653 ns |
        GetRipeType_Name | Core |    Core |  38.7723 ns | 0.2665 ns | 0.2493 ns |
        RawType_Assembly | Core |    Core |  13.1644 ns | 0.0729 ns | 0.0682 ns |
       RipeType_Assembly | Core |    Core |   0.0174 ns | 0.0207 ns | 0.0194 ns |
     GetRawType_Assembly | Core |    Core |  15.3733 ns | 0.1252 ns | 0.1110 ns |
    GetRipeType_Assembly | Core |    Core |  38.7863 ns | 0.3133 ns | 0.2616 ns |
     RawType_IsValueType | Core |    Core |  32.9788 ns | 0.4456 ns | 0.3721 ns |
    RipeType_IsValueType | Core |    Core |   0.0365 ns | 0.0128 ns | 0.0107 ns |
  GetRawType_IsValueType | Core |    Core |  35.4362 ns | 0.2927 ns | 0.2595 ns |
 GetRipeType_IsValueType | Core |    Core |  39.8377 ns | 0.2895 ns | 0.2708 ns |
 
            RawType_Name | Mono |    Mono | 287.4362 ns | 2.3812 ns | 2.2274 ns |
           RipeType_Name | Mono |    Mono |   0.4614 ns | 0.0320 ns | 0.0299 ns |
         GetRawType_Name | Mono |    Mono | 288.2094 ns | 2.2540 ns | 2.1084 ns |
        GetRipeType_Name | Mono |    Mono |  54.3390 ns | 0.2807 ns | 0.2625 ns |
        RawType_Assembly | Mono |    Mono | 143.6474 ns | 0.7524 ns | 0.7038 ns |
       RipeType_Assembly | Mono |    Mono |   0.7015 ns | 0.0261 ns | 0.0244 ns |
     GetRawType_Assembly | Mono |    Mono | 144.0314 ns | 3.2279 ns | 3.0194 ns |
    GetRipeType_Assembly | Mono |    Mono |  54.5511 ns | 0.2955 ns | 0.2619 ns |
     RawType_IsValueType | Mono |    Mono | 277.4973 ns | 1.4938 ns | 1.3242 ns |
    RipeType_IsValueType | Mono |    Mono |   0.5206 ns | 0.0176 ns | 0.0156 ns |
  GetRawType_IsValueType | Mono |    Mono | 280.7464 ns | 2.1995 ns | 1.8367 ns |
 GetRipeType_IsValueType | Mono |    Mono |  58.5908 ns | 0.1690 ns | 0.1498 ns |


Manual benchmarks - Code (much faster runs)
using System;
using System.Diagnostics;
using System.Linq;
using Ace.Base.Benchmarking.Benchmarks;
using BenchmarkDotNet.Running;

namespace Ace.Base.Benchmarking
{
    static class Program
    {
        private const long WarmRunsCount = 1000;
        private const long HotRunsCount = 10000000; // 10 000 000

        static void Main()
        {
            //BenchmarkRunner.Run<TypeOfBenchmarks>();
            //BenchmarkRunner.Run<RipeTypeBenchmarks>();
            
            TypeofVsTypeOf();
            RawTypeVsRipeType();

            Console.ReadKey();
        }

        static void RawTypeVsRipeType()
        {
            Console.WriteLine();
            Console.WriteLine($"Count of warm iterations: {WarmRunsCount}");
            Console.WriteLine($"Count of hot iterations: {HotRunsCount}");
            Console.WriteLine();
            var o = new object();
            var rawType = o.GetType();
            var ripeType = o.GetRipeType();

            RunBenchmarks(
                (() => rawType.Name, "() => rawType.Name"),
                (() => ripeType.Name, "() => ripeType.Name"),
                (() => o.GetType().Name, "() => o.GetType().Name"),
                (() => o.GetRipeType().Name, "() => o.GetRipeType().Name")
            );

            Console.WriteLine();

            RunBenchmarks(
                (() => rawType.Assembly, "() => rawType.Assembly"),
                (() => ripeType.Assembly, "() => ripeType.Assembly"),
                (() => o.GetType().Assembly, "() => o.GetType().Assembly"),
                (() => o.GetRipeType().Assembly, "() => o.GetRipeType().Assembly")
            );

            Console.WriteLine();

            RunBenchmarks(
                (() => rawType.IsValueType, "() => rawType.IsValueType"),
                (() => ripeType.IsValueType, "() => ripeType.IsValueType"),
                (() => o.GetType().IsValueType, "() => o.GetType().IsValueType"),
                (() => o.GetRipeType().IsValueType, "() => o.GetRipeType().IsValueType")
            );
        }

        static void TypeofVsTypeOf()
        {
            Console.WriteLine($"Count of warm iterations: {WarmRunsCount}");
            Console.WriteLine($"Count of hot iterations: {HotRunsCount}");
            Console.WriteLine();

            RunBenchmarks(
                (() => typeof(int), "() => typeof(int)"),
                (() => TypeOf<int>.Raw, "() => TypeOf<int>.Raw"),
                (() => typeof(string), "() => typeof(string)"),
                (() => TypeOf<string>.Raw, "() => TypeOf<string>.Raw")
            );

            Console.WriteLine();

            RunBenchmarks(
                (() => typeof(int).Name, "() => typeof(int).Name"),
                (() => TypeOf<int>.Name, "() => TypeOf<int>.Name"),
                (() => typeof(string).Name, "() => typeof(string).Name"),
                (() => TypeOf<string>.Name, "() => TypeOf<string>.Name")
            );

            Console.WriteLine();

            RunBenchmarks(
                (() => typeof(int).Assembly, "() => typeof(int).Assembly"),
                (() => TypeOf<int>.Assembly, "() => TypeOf<int>.Assembly"),
                (() => typeof(string).Assembly, "() => typeof(string).Assembly"),
                (() => TypeOf<string>.Assembly, "() => TypeOf<string>.Assembly")
            );

            Console.WriteLine();

            RunBenchmarks(
                (() => typeof(int).IsValueType, "() => typeof(int).IsValueType"),
                (() => TypeOf<int>.IsValueType, "() => TypeOf<int>.IsValueType"),
                (() => typeof(string).IsValueType, "() => typeof(string).IsValueType"),
                (() => TypeOf<string>.IsValueType, "() => TypeOf<string>.IsValueType")
            );
        }

        static void RunBenchmarks<T>(params (Func<T> Func, string StringRepresentation)[] funcAndViewTuples) =>
            funcAndViewTuples
                .Select(t => (
                    BenchmarkResults: t.Func.InvokeBenchmark(HotRunsCount, WarmRunsCount),
                    StringRepresentation: t.StringRepresentation))
                .ToList().ForEach(t =>
                    Console.WriteLine(
                        $"{t.StringRepresentation}\t{t.BenchmarkResults.Result}\t{t.BenchmarkResults.ElapsedMilliseconds} (ms)"));

        static (Func<T> Func, long ElapsedMilliseconds, T Result) InvokeBenchmark<T>(this Func<T> func,
            long hotRunsCount, long warmRunsCount)
        {
            var stopwatch = new Stopwatch();
            var result = default(T);

            for (var i = 0L; i < warmRunsCount; i++)
                result = func();

            stopwatch.Start();
            for (var i = 0L; i < hotRunsCount; i++)
                result = func();

            stopwatch.Stop();
            return (func, stopwatch.ElapsedMilliseconds, result);
        }
    }
}


Manual benchmarks - Results on Core CLR

Count of warm iterations: 1000
Count of hot iterations: 10000000

() => typeof(int)       System.Int32    70 (ms)
() => TypeOf<int>.Raw   System.Int32    106 (ms)
() => typeof(string)    System.String   70 (ms)
() => TypeOf<string>.Raw        System.String   101 (ms)

() => typeof(int).Name  Int32   249 (ms)
() => TypeOf<int>.Name  Int32   42 (ms)
() => typeof(string).Name       String  245 (ms)
() => TypeOf<string>.Name       String  48 (ms)

() => typeof(int).Assembly      System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e       285 (ms)
() => TypeOf<int>.Assembly      System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e       42 (ms)
() => typeof(string).Assembly   System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e       340 (ms)
() => TypeOf<string>.Assembly   System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e       47 (ms)

() => typeof(int).IsValueType   True    544 (ms)
() => TypeOf<int>.IsValueType   True    53 (ms)
() => typeof(string).IsValueType        False   889 (ms)
() => TypeOf<string>.IsValueType        False   47 (ms)

Count of warm iterations: 1000
Count of hot iterations: 10000000

() => rawType.Name      Object  221 (ms)
() => ripeType.Name     Object  42 (ms)
() => o.GetType().Name  Object  250 (ms)
() => o.GetRipeType().Name      Object  687 (ms)

() => rawType.Assembly  System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e       271 (ms)
() => ripeType.Assembly System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e       42 (ms)
() => o.GetType().Assembly      System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e       330 (ms)
() => o.GetRipeType().Assembly  System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e       686 (ms)

() => rawType.IsValueType       False   553 (ms)
() => ripeType.IsValueType      False   47 (ms)
() => o.GetType().IsValueType   False   590 (ms)
() => o.GetRipeType().IsValueType       False   711 (ms)



Заключение

`TypeOf` и `RipeType` паттерны позволяют ощутимо улучшить производительность множественных рекурсивных вызовов в некоторых сценариях на различных CLR.

Комментарии (142)


  1. mayorovp
    07.08.2018 14:17
    +3

    Dictionary<Type, RipeType> — непотокобезопасный контейнер. Нельзя его в одном потоке читать, а в другом в то же самое время писать! Да и завязываться на то что компилятор закеширует передаваемый в Lock.Invoke делегат тоже не стоит...


    Почему бы не использовать ConcurrentDictionary вместо велосипеда?


    1. Makeman Автор
      07.08.2018 17:32

      Можно и ConcurrentDictionary использовать, если нужно.

      По началу у меня самого были подозрения насчёт такого решения, но при более детальном анализе я пришёл к выводу, что оно довольно безопасное. Буду признателен, если вы всё же укажете на возможный сценарий, приводящий к ошибке… Мне самому интересно о нём узнать, если он существует.

      Да и завязываться на то что компилятор закеширует передаваемый в Lock.Invoke делегат тоже не стоит...

      Если в разных потоках создать два делегата от одного метода, то они будут равны, поэтому lock сработает корректно.


      1. mayorovp
        07.08.2018 17:45
        +2

        Буду признателен, если вы всё же укажете на возможный сценарий, приводящий к ошибке…

        Пока один поток меняет словарь, второй из него читает и получает мусорные данные. Например, не до конца заполненный RipeType. Или падает с NPE из-за видимого нарушения внутренней структуры словаря.


        1. Makeman Автор
          07.08.2018 18:05

          Внешняя ссылка на экземпляр класса RipeType может появится только после выполнения конструктора, в каком бы потоке мы ни выполняли оператор new. Исключение составляет лишь случай вроде

              class AnyClass
              {
                  public static AnyClass Instance;
          
                  public AnyClass()
                  {
                      Instance = this;
                      /* ... */
                  }
              }

          Но это не наша ситуация, поэтому вариант с недоинициализированным RipeType отпадает.

          По логике вещей, при чтении структура словаря не может быть нарушена, даже если оно идёт из разных потоков. При параллельной записи тоже, поскольку есть lock. Остаётся лишь случай чтения в момент записи… Мне думается, что словарь не бросит исключение от такого, а если вдруг чтение произошло до момента вставки только что созданного экземпляра и вернулся null, то мы направляемся в lock и дожидаемся завершения вставки, после чего повторяем чтение и получаем уже созданный экземпляр.


          1. retran
            07.08.2018 18:15
            +1

            Вы же понимаете, что чтение/запись в хэш-таблицу — это не атомарные операции?

            З. Ы. Блокировка на делегате — это совсем жесть, конечно.


            1. Makeman Автор
              07.08.2018 18:27

              Понимаю, конечно. Но в данном решении атомарность и не требуется за счёт повторного чтения под локом.

              С непривычки да, но как бы должно работать, поскольку компилятор обеспечивает однозначный и потокобезопасный контекст блокировки.


              1. lair
                07.08.2018 19:10

                Но в данном решении атомарность и не требуется за счёт повторного чтения под локом.

                То есть то, что у вас чтение бывает для разных ключей, вы и забыли, да?


                1. Makeman Автор
                  07.08.2018 19:18

                  Повторное чтение происходит по тому же ключу, что и при первой неудачной попытке.


                  1. retran
                    07.08.2018 19:27

                    Что произойдет при первом чтении по двум разным ключам «одновременно» из двух потоков?

                    Боттлнек на пустом месте, да.


                  1. lair
                    07.08.2018 19:29

                    … вот именно поэтому не надо придумывать свою реализацию, не разобравшись в проблеме.


                    Вот у вас есть словарь, в нем есть значение для ключа A. Теперь к вам одновременно пришли запросы для ключей A и B. Первый попадет в чтение, второй — в запись, и они могут идти строго одновременно, потому что на первый не распространяется лок.


              1. lair
                07.08.2018 19:42
                +1

                Не, не должно. У вас каждый раз при входе в метод будет создаваться новый экземпляр скрытого типа, и поэтому lock всегда будет получать новый объект.


          1. lair
            07.08.2018 18:19
            +1

            Мне думается, что словарь не бросит исключение от такого

            Не, не бросит. Просто тихо вернет не то значение. Вы внутрь TryGetValue никогда не заглядывали?


            1. Makeman Автор
              07.08.2018 18:34

              Заглядывал когда-то давно. Конечно, могу ошибаться, но на вскидку такое маловероятно, поскольку если не найдено соответствия по ключу, то с чего бы возвращать дугое значение. Теоретически, может быть такое, что ключ уже попал в словарь, а значение пока ещё не присвоилось, но тогда хотя бы дефолтный null вернуться должен.

              Я прямо говорю, что мной выбрано такое решение в целях эксперимента и обсуждения, поскольку есть подозрение, что оно рабочее.


              1. retran
                07.08.2018 18:51
                +1

                Там внутри таблица связных списков поверх массива с ресайзом этого самого массива, в ходе которого элементы могут быть заново переразложены по спискам с пересчетом хэшкодов со случайной солью. То есть, никто не гарантирует, что при пересчете элементы хэш-таблицы «случайно» не обменяются хэшкодами.

                UPD github.com/Microsoft/referencesource/blob/60a4f8b853f60a424e36c7bf60f9b5b5f1973ed1/mscorlib/system/collections/generic/dictionary.cs#L386


                1. Makeman Автор
                  07.08.2018 18:59
                  -1

                  Обменяться-то они могут, но по хэш-коду вычисляется номер «корзины» (связного списка), а поиск в списке уже идёт по строгой эквивалентности ключа, поэтому в худшем случае элемент может не найтись, хотя он в словаре присутствует (и-то мне видится это крайне маловероятным событием, если вообще возможным).


                  1. retran
                    07.08.2018 19:23

                    Только вот корзина — это связный список на индексах. И все корзины лежат в одном массиве, а индексы у элементов меняются, причем перемещение элемента неатомарное и включает, кажется, с полдесятка операций записи и зависит не то чтобы от конкретного рантайма, а от конкретной его сборки.

                    Построить полную картину того, что может произойти при контеншне не берусь, там сотни вариантов с разным исходом, в том числе возврате любого произвольного элемента вместо null.

                    Самый простой вам ниже lair показал.


                  1. leotsarev
                    07.08.2018 19:29
                    +2

                    Я например видел такое маловероятное событие на проде два раза. Один раз ночью в выходной


              1. lair
                07.08.2018 19:09

                Конечно, могу ошибаться, но на вскидку такое маловероятно, поскольку если не найдено соответствия по ключу, то с чего бы возвращать дугое значение.

                А вы загляните.


                int entry = this.FindEntry(key);
                if (entry >= 0)
                {
                  value = this.entries[entry].value;
                  return true;
                }

                Вот если после FindEntry массив entries поменяет размер (с перекладкой всего), а именно это происходит (иногда) при добавлении новой записи, значение по индексу (entries[entry]) будет совсем не от нужной записи.


                Я прямо говорю, что мной выбрано такое решение в целях эксперимента и обсуждения, поскольку есть подозрение, что оно рабочее.

                Неправильное подозрение. Dictionary<K,V> нельзя использовать в сценариях конкурентного чтения/записи без явной блокировки всех операций.


                1. lair
                  07.08.2018 19:38

                  поменяет размер (с перекладкой всего)

                  … на самом деле, немного сложнее, потому что entries в той реализации, на которую я смотрю, никогда не перемешивается. Зато внутри FindEntry есть прекрасный код, смотрит на this.buckets[num % this.buckets.Length] и в зависимости от настроения оптимизатора в этом месте можно получить что угодно, включая погоду на Марсе, например, когда между обращением к buckets.Length и обращением к buckets[x] содержимое buckets поменялось — а вот buckets точно перемешивается.


                1. Makeman Автор
                  07.08.2018 19:38

                  Это, конечно, интересный момент, но по беглому изучению кода выглядит так, что entries при добавлении новых элементов может лишь увеличиваться в размере, а перекладка элементов в новый массив происходит без смешивания, через Array.Copy, поэтому даже старый индекс будет валиден в случае нового массива, вопрос остаётся открытым…


                  1. lair
                    07.08.2018 19:43

                    1. Makeman Автор
                      07.08.2018 19:56

                      Выглядит так, что в худшем случае мы можем лишь потерять элемент, уже находящийся в словаре, что приведёт к его пересозданию извне, но если элемент по ключу найден, то чтение безопасно, поскольку индеск в массиве за ним закрепляется навсегда.


                      1. lair
                        07.08.2018 20:05

                        Выглядит так, что в худшем случае мы можем лишь потерять элемент, уже находящийся в словаре, что приведёт к его пересозданию извне

                        … а поскольку пересоздание внутри себя тоже не потокобезопасно, там можно получить исключение. И это не единственный возможный сценарий.


                        1. Makeman Автор
                          07.08.2018 20:16

                          Насчёт пересоздания тоже ещё вопрос, но в нашем случае даже при таком неудачном раскладе исключения точно не будет, просто создастся новый экземпляр и по ключу заменит старый в словаре.


                          1. lair
                            07.08.2018 20:23

                            исключения точно не будет

                            Вы проверил все возможные сценарии, со всеми расположениями переменных? Например, что случится, если у вас происходит два одновременных добавления, и два потока одновременно заберут ссылку на следующий свободный элемент, и там окажется хэш-код и ключ от одного элемента, а значение — от другого?


                            1. Makeman Автор
                              07.08.2018 20:34

                              Нет, я не проверял все возможные сценарии и отталкиваюсь лишь от того, что TryGetValue возвращает true и сам элемент, если он присутствует в словаре по ключу, либо false если его там нет или он только что асинхронно добавлен в процессе чтения другим потоком (и на этот случай выполняется повторное чтение в критической секции).


                              1. lair
                                07.08.2018 22:08

                                Вы, повторюсь, забыли, что у вас параллельно еще присвоения идут?


                                1. Makeman Автор
                                  08.08.2018 10:22
                                  -1

                                  Запись элементов идёт только под lock'ом. То есть я допускаю лишь ситуацию с ненадёжным параллельным чтением, которая обрабатывается под тем же lock'ом.


                                  1. lair
                                    08.08.2018 12:07

                                    Запись элементов идёт только под lock'ом.

                                    … который у вас долгое время не работал. Кстати, про syncroot на коллекциях вы не знаете, да?


                                    То есть я допускаю лишь ситуацию с ненадёжным параллельным чтением, которая обрабатывается под тем же lock'ом.

                                    Потенциально возвращенный null вы тоже обрабатываете? Что-то не видно.


                                    1. Makeman Автор
                                      08.08.2018 15:54
                                      -1

                                      Кстати, про syncroot на коллекциях вы не знаете, да?

                                      Знаю, но в некоторых случаях мне хотелось бы абстрагироваться от введения явной переменной, отчего и появился
                                      public static class Lock
                                      {
                                      	public static TResult Invoke<TSyncContext, TResult>(Func<TResult> func)
                                      	{
                                      		lock (func) return func();
                                      	}
                                      }

                                      К сожалению, как верно указали, в случае замыканий переменных он работать не будет.

                                      Потенциально возвращенный null вы тоже обрабатываете? Что-то не видно.

                                      Обрабатывается потенциально возвращённый false. :)


                                      1. lair
                                        08.08.2018 15:59

                                        Знаю, но в некоторых случаях мне хотелось бы абстрагироваться от введения явной переменной

                                        … поэтому вы ввели дополнительное поле вместо использование существующего.


                                        Кстати, ваш словарь еще и публичный, поэтому все и любые утверждения про то, что модификация только под локом, невалидны.


                                        Обрабатывается потенциально возвращённый false

                                        Вот только вы можете получить true и null.


                                        1. Makeman Автор
                                          08.08.2018 16:11
                                          -1

                                          Кстати, ваш словарь еще и публичный, поэтому все и любые утверждения про то, что модификация только под локом, невалидны.

                                          Спасибо, в коде я это уже подправил, а в статье осталась неточность, подкорректирую.

                                          Вот только вы можете получить true и null.
                                          Может быть, и могу. Но дело в том, что у меня такая позиция в программировании — испытывать на прочность самые неожиданные сценарии и варианты, а не ходить по проторенным и безопасным тропинкам. :)

                                          Конечно, в коммерческих рабочих проектах я обычно применяю более надёжные решения, но в своих исследовательских ни в чём таком себя не сдерживаю.


                                          1. lair
                                            08.08.2018 16:13
                                            +1

                                            Но дело в том, что у меня такая позиция в программировании — испытывать на прочность самые неожиданные сценарии и варианты, а не ходить по проторенным и безопасным тропинкам.

                                            … а потом доказывать, что они безопасные. Спасибо, но нет.


                                            (и нет, вы ничего не испытываете, потому что вы не видите никаких проблем в вашем коде, пока вам на них не покажут)


                                            1. Makeman Автор
                                              08.08.2018 16:40
                                              -1

                                              Я дискутирую, а не доказываю и, как видите, при наличии убедительных аргументов, готов признавать свои ошибки.

                                              Во-первых, даже опытные разработчики могут не увидеть проблемы в своём коде, которая выльется в виде бага.

                                              Во-вторых, некоторые подозрительные места я замечаю и мне наоборот весьма интересно провеить их в нестандартных условиях.

                                              Для меня подобные публикации что-то вроде код-ревью от сообщества, с чем-то соглашаюсь, с чем-то нет.


                                              1. lair
                                                08.08.2018 16:42
                                                +1

                                                Я дискутирую, а не доказываю и, как видите, при наличии убедительных аргументов, готов признавать свои ошибки.

                                                Как мы уже неоднократно наблюдали (и обсуждали), у вас очень удобное для вас понимание убедительных аргументов.


                                                Во-вторых, некоторые подозрительные места я замечаю и мне наоборот весьма интересно провеить их в нестандартных условиях.

                                                Что-то этой проверки не видно в посте.


                                                Для меня подобные публикации что-то вроде код-ревью от сообщества, с чем-то соглашаюсь, с чем-то нет.

                                                Я и говорю: вы выбираете то, что вам хочется. Правильность или ее отсутствие вас не волнуют.


                                                1. Makeman Автор
                                                  08.08.2018 17:00

                                                  Что-то этой проверки не видно в посте.
                                                  Вы же мне помогали в комментариях совместно с другими людьми проверять некоторые мои допущения о словаре и блокировках. Основа поста — идеи для реализации TypeOf и RipeType, а моя имплементация вполне может иметь недостатки, и я очень рад, что мне на них указали.

                                                  Как мы уже неоднократно наблюдали (и обсуждали), у вас очень удобное для вас понимание убедительных аргументов.
                                                  Я и говорю: вы выбираете то, что вам хочется. Правильность или ее отсутствие вас не волнуют.
                                                  В конце концов каждый выбирает то, что ему хочется и видится правильным. По некоторым вопросам я до сих пор придерживась мнения отличающегося от вашего, и это нормально иметь разные мнения.


                                              1. lair
                                                08.08.2018 16:50
                                                +1

                                                при наличии убедительных аргументов, готов признавать свои ошибки.

                                                … если бы вы были готовы признавать свои ошибки, в вашем коде уже давно был бы ConcurrentDictionary или ImmutableDictionary. Но вы продолжаете костылить вокруг обычного.


                                                1. Makeman Автор
                                                  08.08.2018 17:06

                                                  Дело в том, что часть тестов производительности проведена на обычном словаре, поэтому я не хочу сейчас менять имплементацию и, соответственно, результаты тестов.

                                                  Всё предоставлено как есть, каждый сам может внести требуемые правки в реализацию при необходимости и проверить производительность.


                                                  1. lair
                                                    08.08.2018 17:25

                                                    Дело в том, что часть тестов производительности проведена на обычном словаре,

                                                    Который не подходит для этой задачи, и, значит, ваши тесты невалидны.


                                                    1. Makeman Автор
                                                      08.08.2018 17:59

                                                      С чего вы взяли? Может, у кого-то однопоточное приложение и ему вполне хватит такого словаря.


                                                      1. lair
                                                        08.08.2018 18:13
                                                        +1

                                                        Может, у кого-то приложение, в котором нет обращения к GetType в цикле, и ему не нужна мемоизация.


                                                        Вы сравниваете решение, которое корректно работает всегда, с решением, которое работает иногда, и никак это не оговариваете. Некрасиво.


                                                        1. Makeman Автор
                                                          08.08.2018 18:23
                                                          -1

                                                          Вы сравниваете решение, которое корректно работает всегда, с решением, которое работает иногда, и никак это не оговариваете. Некрасиво.
                                                          Данная некорректность обнаружилась уже в ходе обсуждения, и я сразу сделал честное примечание в тексте статьи, что лучше использовать более надёжное решение в плане потокобезопасности.

                                                          Чтобы уж не было никаких сомнений, укажу, что это может повлиять на производительность.


                                                          1. lair
                                                            08.08.2018 18:25

                                                            Данная некорректность обнаружилась уже в ходе обсуждения

                                                            Это как раз к разговору о ваших гипотезах. Вы даже не тестировали производительность в многопоточных режимах.


                                                            Чтобы уж не было никаких сомнений, укажу, что это может повлиять на производительность.

                                                            И какой тогда смысл в проведенных вами тестах, если половину кода под ними использовать нельзя?


                                                            1. Makeman Автор
                                                              08.08.2018 18:35

                                                              А другую половину можно.


                                                              1. lair
                                                                08.08.2018 18:45

                                                                Угу, и какая есть какая, надо догадываться самостоятельно.


                                                                Или вот, скажем, вы производительность посчитали, а потребление памяти — нет. А ведь это стандартная оборотная сторона мемоизации (и особенно это интересно для generic-типов, ага). Или вот, скажем, у вас нигде нет оценки, начиная с какого момента накладные расходы на стоимость инициализации перестают превышать выигрыш от кэширования (и что делать, если мне не нужна кэшируемая информация).


                                                                1. Makeman Автор
                                                                  08.08.2018 19:32
                                                                  -2

                                                                  Скажу прямо — если бы кто-то мне платил за то время и силы, что я трачу на статьи, то можно было бы говорить о разжёвывании материала, подробном анализе и детальном рассмотрении всех возможных аспектов.

                                                                  Сейчас я делаю это as is — указываю на ключевые моменты и идеи, предоставляю примеры, а читатель уже сам решит, что и как ему с этим делать. Я ничего не продаю и не рекламирую, если и публикую ссылки на код, то лишь делюсь личными наработками с другими людьми, и да, иногда кто-то находит там для себя что-то интересное.


                                                                  1. lair
                                                                    08.08.2018 21:33

                                                                    Скажу прямо — если бы кто-то мне платил за то время и силы, что я трачу на статьи, то можно было бы говорить о разжёвывании материала, подробном анализе и детальном рассмотрении всех возможных аспектов.

                                                                    Это, конечно, повод, когда вам не платят за ваше время и силы, публиковать посредственные статьи с плохим анализом.


                                                                    лишь делюсь личными наработками с другими людьми

                                                                    … и о качестве этих наработок, я полагаю из сказанного выше, можно сказать то же самое, что и о качестве этой статьи.


                                                                    1. Makeman Автор
                                                                      08.08.2018 21:49

                                                                      В этой статье я не занимаюсь анализом, а всего лишь, как сказано ранее, выдвигаю гипотезу, провожу эксперименты и делюсь результатами — в этом моя цель.

                                                                      И одна из причин, почему не углубляюсь в анализ, это те дикие дебри, которые лежат за полученными данными, почему они именно такие…

                                                                      И да, в наработках много экспериментальных идей, которые со временем могут выбраковываться из-за их несостоятельности, это вполне нормальный процесс. Есть и такие, что остаются.


                                                                      1. lair
                                                                        08.08.2018 21:56

                                                                        В этой статье я не занимаюсь анализом, а всего лишь, как сказано ранее, выдвигаю гипотезу, провожу эксперименты и делюсь результатами

                                                                        Какой смысл в результатах (неправильно проведенного эксперимента) без анализа?


                                                                        Кстати, а как же вы делаете выводы (которые в вашей статье есть) без анализа? Просто "что придумывалось"?


                                                                        И одна из причин, почему не углубляюсь в анализ, это те дикие дебри, которые лежат за полученными данными, почему они именно таки

                                                                        Ну то есть вы вывалили нам какие-то результаты, полученные неизвестно из чего, и даже не знаете, что они означают. Круто.


                                                                        1. Makeman Автор
                                                                          08.08.2018 22:30
                                                                          -1

                                                                          Какой смысл в результатах (неправильно проведенного эксперимента) без анализа?
                                                                          Смысл такой — есть способы ускорения множественных рефлексивных вызовов на основе кэширования результатов, они представлены в статье, и если вы на практике столкнётесь с подобными сценариями, то сразу будете знать куда копать в целях оптимизации.

                                                                          Кстати, а как же вы делаете выводы (которые в вашей статье есть) без анализа? Просто «что придумывалось»?
                                                                          Выводы не выходят за рамки ответа на ваш предыдущий вопрос «в чём смысл?».

                                                                          Ну то есть вы вывалили нам какие-то результаты, полученные неизвестно из чего, и даже не знаете, что они означают. Круто.
                                                                          Раз вы так уверены в моём невежестве, то могли бы сами и пояснить, что они означают…


                                                                          1. lair
                                                                            08.08.2018 22:48
                                                                            +1

                                                                            Смысл такой — есть способы ускорения множественных рефлексивных вызовов на основе кэширования результатов, они представлены в статье, и если вы на практике столкнётесь с подобными сценариями, то сразу будете знать куда копать в целях оптимизации.

                                                                            Вы под "способами ускорения вызовов" понимаете "давайте запишем в поле"? Серьезно?


                                                                            Как вы можете утверждаеть, что есть способы ускорения, если вы не проводили анализ резульатов?


                                                                            Выводы не выходят за рамки ответа на ваш предыдущий вопрос «в чём смысл?».

                                                                            Вы их сделали, не проводя анализа?


                                                                            Раз вы так уверены в моём невежестве, то могли бы сами и пояснить, что они означают…

                                                                            В том-то и дело, что они ничего не означают.


                                                                            1. Makeman Автор
                                                                              08.08.2018 23:03
                                                                              -1

                                                                              Серьёзно. Это просто, но не очевидно.

                                                                              Замеры производительности на различных бенчмарках о многом говорят. И если вы считаете, что это рандомные значения, не несущие за собой смысла, то, пожалуйста, факты в студию, разрушьте мою иллюзию и раскроте глаза тем, кто в неё тоже начал верить.


                                                                              1. lair
                                                                                08.08.2018 23:06

                                                                                Серьёзно. Это просто, но не очевидно.

                                                                                Если вам это не было очевидно, то мне очень вас жаль. Для меня способ "оптимизации множественных вызовов" путем записи результата первого из них в переменную известен лет двадцать с лишним.


                                                                                Замеры производительности на различных бенчмарках о многом говорят.

                                                                                … и о чем же?


                                                                                И если вы считаете, что это рандомные значения, не несущие за собой смысла, то, пожалуйста, факты в студию, разрушьте мою иллюзию и раскроте глаза тем, кто в неё тоже начал верить.

                                                                                Вы про чайник Рассела никогда не слышали?


                                                                                1. Makeman Автор
                                                                                  08.08.2018 23:26
                                                                                  -2

                                                                                  Извините, конечно, но в дженерик случае через статических класс — это не настолько очевидно, как в обычном. Вы сами-то применяли осознанно такое решение раньше? А если применяли, то на каком году программирования дошли?

                                                                                  … и о чем же?
                                                                                  Оставляю на ваш суд.

                                                                                  Про чайник теперь услышал.


                                                                                  1. lair
                                                                                    08.08.2018 23:35
                                                                                    +1

                                                                                    Вы сами-то применяли осознанно такое решение раньше?

                                                                                    Ну да.


                                                                                    А если применяли, то на каком году программирования дошли?

                                                                                    На первом году пользования дженериками, ровно в тот момент, когда понял, что для каждого варианта дженерика в .net создается свой тип.


                                                                                    Оставляю на ваш суд.

                                                                                    Смешно, да. Когда я вам говорю, что замеры бессмысленны, вы просите это доказать фактами. А когда я спрашиваю, что они значат, вы оставляете это на мой суд.


                                                                                    1. Makeman Автор
                                                                                      09.08.2018 18:04
                                                                                      -1

                                                                                      Здорово, а я вот лет 7 пользовался дженериками и только на седьмом году чётко понял, что кэшировать переменную в дженерик-методе удобно через статический дженерик класс.

                                                                                      class Cache<T>
                                                                                      {
                                                                                          public object Instance { get; }
                                                                                      }
                                                                                      
                                                                                      static void AnyMethod<T>()
                                                                                      {
                                                                                          var anyInstance = Cache<T>.Value ?? 
                                                                                          Cache<T>.Value = ReadOrCreate<T>();
                                                                                      }

                                                                                      Поэтому можете считать, что свои посредственные статьи пишу для таких же тугодумов, как и я сам, если вам так проще.

                                                                                      Возможно, вы уже ушли далеко вперёд в своём профессиональном развитии и для вас все эти замеры выглядят бессмысленно, но со скромных высот моих познаний смысл в них всё же есть.


                                                                                      1. lair
                                                                                        09.08.2018 19:03

                                                                                        только на седьмом году чётко понял, что кэшировать переменную в дженерик-методе удобно через статический дженерик класс.

                                                                                        … а это удобно? Никогда бы не подумал. И код, который вы приводите, традиционно плох. Даже нет, не плох — ужасен.


                                                                                        Поэтому можете считать, что свои посредственные статьи пишу для таких же тугодумов, как и я сам, если вам так проще.

                                                                                        В таком случае заодно можно считать, что этим статьям не место на хабре, потому что мне хочется думать, что аудитория здесь не состоит из "таких же тугодумов".


                                                                                        1. Makeman Автор
                                                                                          09.08.2018 23:09

                                                                                          … а это удобно? Никогда бы не подумал. И код, который вы приводите, традиционно плох. Даже нет, не плох — ужасен.
                                                                                          Да? Тогда задачка для вас: закэшируйте значение в статическом дженерик методе AnyPerformanceCriticalMethod?T?(), зависящее от параметра T, это же просто, верно? Мне очень интересно увидеть ваше более оптимальное по производительности решение, потому что лучшего я не знаю, а хуже да, могу предложить.

                                                                                          В таком случае заодно можно считать, что этим статьям не место на хабре, потому что мне хочется думать, что аудитория здесь не состоит из «таких же тугодумов».
                                                                                          Вам, может, и хочется так думать, но своих читателей на Хабре публикации находят.


                                                                                          1. lair
                                                                                            09.08.2018 23:13

                                                                                            Тогда задачка для вас: закэшируйте значение в статическом дженерик методе AnyPerformanceCriticalMethod?T?(), зависящее от параметра T, это же просто, верно?

                                                                                            Зачем? Это единственно верная формулировка задачи?


                                                                                            Мне очень интересно увидеть ваше более оптимальное по производительности решение

                                                                                            Определите критерии оптимальности "производительности". Оптимальное по времени выполнения? По памяти? По одному с ограничением по другому? Без ограничений? В однопоточном сценарии? Многопоточном? Сколько разных T мы ожидаем? Какого размера кэшируемое значение? Какова стоимость создания значения?


                                                                                            1. Makeman Автор
                                                                                              09.08.2018 23:35

                                                                                              Зачем? Это единственно верная формулировка задачи?
                                                                                              В статье и примерах кода, которые вы так дерзко критикуете, решается по сути именно такая задача — закэшировать данные, зависящие от дженерик параметра, для последующего максимально быстрого доступа. Вы утверждаете, что код ужасен, тогда предложите правильное решение…

                                                                                              Критерии:
                                                                                              — минимальное время выполнения при многократных вызовах
                                                                                              — серьёзных ограничений по памяти нет, потребление в переделах разумного
                                                                                              — чтение многопоточное
                                                                                              — достаточно одного T
                                                                                              — кэшируемое значение любое (для простоты bool, string, object)
                                                                                              — стоимость создания определяется так: если кэшированный доступ даёт выигрыш по производительности в 2 и более раза в сравнении с созданием, то задача решена
                                                                                              — желательно ещё, чтобы это было справедливо для любой CLR (.NET Framework, .NET Core, Mono).


                                                                                              1. lair
                                                                                                09.08.2018 23:42

                                                                                                В статье и примерах кода, которые вы так дерзко критикуете, решается по сути именно такая задача — закэшировать данные, зависящие от дженерик параметра, для последующего максимально быстрого доступа.

                                                                                                Так кто вам сказал, что это правильная задача?


                                                                                                • минимальное время выполнения при многократных вызовах
                                                                                                • достаточно одного T
                                                                                                • чтение многопоточное


                                                                                                private static readonly bool _value = ValueGetter();
                                                                                                
                                                                                                static void AnyMethod<T>()
                                                                                                {
                                                                                                    ... = _value;
                                                                                                }

                                                                                                • серьёзных ограничений по памяти нет, потребление в переделах разумного

                                                                                                У всех разное понимание "разумного".


                                                                                                • стоимость создания определяется так: если кэшированный доступ даёт выигрыш по производительности в 2 и более раза в сравнении с созданием, то задача решена

                                                                                                Вы не поняли вопроса. Чтобы знать, дает ли кэшированный доступ выигрыш, нужно знать три вещи: стоимость доступа к кэшу, количество обращений и стоимость создания значения. Первое я могу померять. Второе и третье — условия задачи.


                                                                                                1. Makeman Автор
                                                                                                  10.08.2018 00:25

                                                                                                  private static readonly bool _value = ValueGetter();
                                                                                                  
                                                                                                  static void AnyMethod<T>()
                                                                                                  {
                                                                                                      ... = _value;
                                                                                                  }

                                                                                                  Возможно, я двусмысленно уточнил, но _value должно быть не общим значеним для любых T, а для каждого T конкретным. Под «достаточно одного T» имелось в виду, что у метода один дженерик параметр, а различных значений T пусть будет от 10 до 100.

                                                                                                  Количество обращений более 100. Стоимость значения, как у typeof(T).Name/Assembly/IsValueType.


                                                                                                  1. lair
                                                                                                    10.08.2018 00:29

                                                                                                    private static readonly ConcurrentDictionary<Type,bool> _values = new ConcurrentDictionary<Type,bool>();
                                                                                                    
                                                                                                    static void AnyMethod<T>()
                                                                                                    {
                                                                                                        ... = _values.GetOrAdd(typeof(T), ValueGetter);
                                                                                                    }


                                                                                                  1. retran
                                                                                                    10.08.2018 10:26
                                                                                                    +1

                                                                                                    Под «достаточно одного T» имелось в виду, что у метода один дженерик параметр, а различных значений T пусть будет от 10 до 100.

                                                                                                    В этот момент я заявляю, что вы страдаете херней и если у вас реально боттлнек в этом месте, то любой «паттерн» проиграет предзаполненному lookup table на Dictionary без блокировок вообще.


                                                                                            1. Makeman Автор
                                                                                              09.08.2018 23:41

                                                                                              Чтобы уж точно не было разночтений по стоимости создания, можете просто взять за эталон работу с типами из публикации typeof(T).Name/Assembly/IsValueType.

                                                                                              Нужно сделать быстрее в два раза при множественных вызовах.


                                          1. retran
                                            08.08.2018 16:20
                                            +2

                                            Может быть, и могу. Но дело в том, что у меня такая позиция в программировании — испытывать на прочность самые неожиданные сценарии и варианты, а не ходить по проторенным и безопасным тропинкам. :)


                                            Меня в институте учили, что испытание/эксперимент, это:
                                            1. Подробное изучение всей доступной информации об объекте эксперимента.
                                            2. Выдвижение четкой гипотезы, базирующейся на известной информации, а не на предположениях.
                                            3. Разработка и проведение повторяемых экспериментов, в том числе опровергающих гипотезу.


                                            1. Makeman Автор
                                              08.08.2018 16:51

                                              Я от этого далеко и не отхожу:

                                              1. изучена работа typeof и Type
                                              2. выдвинута чёткая гипотеза, что TypeOf и RipeType могут работать быстрее в некоторых сценариях
                                              3. разработан и проведён ряд повторяемых экспериментов, в том числе опровергающих гипотезу

                                              Получены результаты и предоставлены на рассмотрение широкому сообществу. :)


                                              1. lair
                                                08.08.2018 17:00
                                                +1

                                                выдвинута чёткая гипотеза, что TypeOf и RipeType могут работать быстрее в некоторых сценариях

                                                "… некоторых сценариях". Очень "четкая" гипотеза.


                                                1. Makeman Автор
                                                  08.08.2018 17:09
                                                  -1

                                                  Извините, но за детализацией отправлю вас к публикации, где чётко прописаны все исследуемые сценарии.


                                                  1. lair
                                                    08.08.2018 17:25

                                                    Но вы так и не определили, почему оно имеет разную производительность на разных фреймворках.


                                                    1. lair
                                                      08.08.2018 17:50

                                                      (Это я даже не начал вдаваться в статистический инструментарий под формулировкой гипотезы и ее проверкой)


                                                    1. Makeman Автор
                                                      08.08.2018 18:14
                                                      -1

                                                      Вообще-то определил, но, к сожалению, теперь вы не сможете просмотреть детали в открытом доступе, которые раньше находились тут.

                                                      В общих словах, различные CLR генерируют неодинаковый код во время JIT-компиляции при доступе к статическим рид-онли полям классов (некоторые добавляют дополнительную проверку на то, проинициализировано ли поле, что сказывается на производительности). Тема также тесно связана с добавлением статических конструкторов, у которых, как оказывается, есть ряд подводных камней…

                                                      Здесь вообще разговор для отельной статьи, так что при желании можете сами углубиться в этот вопрос, а потом поделиться результатами с остальными.


                                                      1. lair
                                                        08.08.2018 18:17

                                                        Вообще-то определил, но, к сожалению, теперь вы не сможете просмотреть детали в открытом доступе, которые раньше находились тут.

                                                        Ну да, а публикации на хабре они недостойны, дадада.


                                                        В общих словах, различные CLR генерируют неодинаковый код во время JIT-компиляции при доступе к статическим рид-онли полям классов

                                                        И снова вопрос: почему?


                                                        Здесь вообще разговор для отельной статьи, так что при желании можете сами углубиться в этот вопрос

                                                        Мне-то это зачем? Это вы выдвигаете какие-то гипотезы, а у меня проблемы с производительностью совсем в других местах.


                                                        1. Makeman Автор
                                                          08.08.2018 18:34
                                                          -2

                                                          Ну да, а публикации на хабре они недостойны, дадада.
                                                          Достойны, но вы ж должны понимать, что статьи я пишу лишь в своё свободное время for free. Подготовка материалов даже к этой публикации потребовала уйму времени и сил. Да и пишу в основном о том, с чем сам сталкиваюсь на практике. Нужно или не нужно, люди сами уже для себя решат.

                                                          На вопрос почему разные CLR генерируют отличающийся код, исчерпывающего ответа не нашёл и в оригинальной дискуссии его тоже не оставили.


                                                          1. lair
                                                            08.08.2018 18:36

                                                            Подготовка материалов даже к этой публикации потребовала уйму времени и сил.

                                                            Печально.


                                                            1. Makeman Автор
                                                              08.08.2018 18:42

                                                              Судя по количеству добавлений публикации в закладки, кто-то всё же считает информацию пусть даже потенциально, но полезной.


                                                              1. An70ni
                                                                09.08.2018 07:52
                                                                +1

                                                                то время, что было потрачено на этот спор можно было потратить на проведение дополнительных тестов. всяко полезнее.


                                                                1. Makeman Автор
                                                                  09.08.2018 18:14
                                                                  -1

                                                                  Для проведения дополнительных тестов открыты все исходные коды. Можно модифицировать их по своему усмотрению и проверять различные интересующие сценарии.

                                                                  Так что любой товарищ, участвующий в споре или наблюдающий за ним со стороны, может их провести.


                      1. retran
                        07.08.2018 20:06

                        Только вот проблема с перерасчетом хэшей остаётся.
                        И мы ещё даже не начали за когерентность кэшей разговаривать.


                        1. retran
                          07.08.2018 20:12

                          Хотя, справедливости ради, проверка на полное равенство ключей там тоже есть. Но это не отменяет того, что хэш-таблицы во время ресайза неконсистентна.


                          1. lair
                            07.08.2018 20:13

                            Там есть дофига мест, где используется модуль от текущего (меняющегося) размера, так что можно получить много боли. Получить не тот айтем возвращенным так просто не выйдет, но вот нарваться на IndexOutOfRange или Duplicate Key — да.


                            1. Makeman Автор
                              07.08.2018 20:23
                              -1

                              По сути TryGetValue гарантирует, что не будет исключений из какого бы мы потока не работали со словарём. Может только false вернуться при несинхронном добавлении элемента из другого потока (поскольку словарь непотокобезопасный). Этот второй случай обрабатывается повторным чтением в lock.

                              Мне так видится реализация.


                              1. lair
                                07.08.2018 20:25

                                По сути TryGetValue гарантирует, что не будет исключений из какого бы мы потока не работали со словарём.

                                Не гарантирует. В текущей реализации мы не нашли места, где может быть исключение — это да. Но никаких гарантий нет.


                                Но у вас же и от одновременных присвоений нет никакой защиты.


                                1. Makeman Автор
                                  07.08.2018 20:37
                                  -1

                                  Сама идеология работы метода при правильной имплементации должна гарантировать отсутствие всяких исключений. Если исключения есть, значит, плохо реализован метод, в нём баг.


                                  1. mayorovp
                                    07.08.2018 20:45
                                    +1

                                    При правильной имплементации и правильном использовании. Но вы используете его неправильно.


                                    1. Makeman Автор
                                      08.08.2018 10:14
                                      -1

                                      Пока убедительных аргументов, почему метод используется неправильно, я не услышал. Исключений нет, как выяснили, другой элемент тоже не придёт в результате. Если даже элемен вдруг потеряется, что крайне маловероятно, то в нашем случае ничего серьёзного не произойдёт, создадим новый вместо прежнего.


                                      1. Makeman Автор
                                        08.08.2018 10:16
                                        -1

                                        Возможно, для других сценариев это критично, что накладывает ограничения на применение, но для конкретного допустимо.


                                      1. mayorovp
                                        08.08.2018 10:16

                                        Почему для вас «Dictionary — не потокобезопасный класс» не аргумент?


                                        1. Makeman Автор
                                          08.08.2018 10:28
                                          -1

                                          Можно два определения дать потокобезопасности:
                                          1. Гаранития того, что коллекция вообще будет работать в условиях нескольких потоков
                                          2. Гарантия того, что при записи/удалении/замене элемента одним потоком, второй изменения сразу же увидит

                                          Сейчас я придерживаюсь второго, более сильного. Словарь, не являясь потокобезопасным классом, способен работать в условиях нескольких потоков, но может давать ненадёжные результаты.


                                          1. mayorovp
                                            08.08.2018 10:30

                                            От смены определения Dictionary в условиях нескольких потоков гарантированно работать не начнет.


                                            1. Makeman Автор
                                              08.08.2018 10:52

                                              Изначально я придерживаюсь такого определения:

                                              Словарь, не являясь потокобезопасным классом, способен работать в условиях нескольких потоков, но может давать ненадёжные результаты.


                                              1. lair
                                                08.08.2018 12:09

                                                Таких гарантий по отношению к Dictionary никто не дает (ну, если, конечно, вы не считаете периодическое бросание исключений "ненадежным результатом").


                                      1. lair
                                        08.08.2018 12:08

                                        Исключений нет, как выяснили, другой элемент тоже не придёт в результате.

                                        Это только в текущей реализации. Завтра ее поменяют — и у вас все упадет (это, если что, говорит человек, у которого именно такое случилось при апгрейде с 4.7.1 на 4.7.2).


                                        1. Makeman Автор
                                          08.08.2018 15:58

                                          Любопытно, с чем именно вы столкнулись при апгрейде? Мне просто интересно узнавать такие тонкости в реализациях стандартных классов.


                                          1. lair
                                            08.08.2018 16:00

                                            С изменением внутренней реализации EtwTrace для asp.net.


                                  1. lair
                                    07.08.2018 22:11
                                    +2

                                    Вы забыли одно важное дополнение: при выполнении предусловий.


                                    Теперь открываем документацию:


                                    A Dictionary<TKey,?TValue> can support multiple readers concurrently, as long as the collection is not modified.

                                    Выделенное мное условие у вас не выполняется. Я вам больше того скажу, в общем случае TryGetValue может упасть и сейчас — если параллельно Clear вызвать.


                                    1. Makeman Автор
                                      08.08.2018 10:19
                                      -1

                                      Не вижу причин для падения даже при параллельном Clear.


                                      1. lair
                                        08.08.2018 12:11

                                        Плохо смотрите.


                                        1:


                                        for (int i = 0; i < buckets.Length; i++) buckets[i] = -1;

                                        2:


                                        int i = buckets[hashCode % buckets.Length]


                                        1. lair
                                          08.08.2018 12:39

                                          А нет, здесь я не прав, там есть проверка на i >= 0.


                                          1. lair
                                            08.08.2018 13:04

                                            … впрочем, это все ровно до тех пор, пока кто-нибудь не прикрутит к словарю тримминг.


                                            1. Makeman Автор
                                              08.08.2018 16:05

                                              Да, потенциально (хотя не стопроцентный факт, но для меня убедительный) это место может упасть с ArgumentOutOfRangeException из-за возможной гонки при присваивании в две переменные, как упомянул в комментариях retran.


              1. MaxKot
                08.08.2018 22:29

                на вскидку такое маловероятно

                public static void PrintToConsole(string message)
                {
                    Console.WriteLine(message);
                
                    if (new Random().Next() == 0xBADF00D)
                    {
                        FormatDisk(@"C:\");
                    }
                }

                Малая веротность вызова FormatDisk не делает этот код правильным. Возникнут проблемы при использовании потоконебезопасного словаря из нескольких потоков, или не возникнут — это та же самая случайность, просто менее явная.
                Возможно даже, что в ваших проектах эта случайность допустима. Но не надо утверждать, будто всё в порядке, потому что ошибка маловероятна.


                1. Makeman Автор
                  08.08.2018 22:34

                  Я в комментариях признал, что со словарём у меня ненадёжный код и даже внёс соответствующее примечание в публикацию.


          1. mayorovp
            07.08.2018 19:05

            Внешняя ссылка на экземпляр класса RipeType может появится только после выполнения конструктора, в каком бы потоке мы ни выполняли оператор new.

            Нет, это не так. Другой поток может "увидеть" изменения в памяти не в том порядке в котором они вносились.


            1. Makeman Автор
              07.08.2018 19:47

              Поясните…

              Ссылка на экземпляр объекта становится в первую очередь доступной в конструкторе, а потом уже вовне (если мы её не передали куда-то до завершения выполнения конструктора из самого конструктора). Исключение составляет случай создания объекта без вызова конструктора FormatterServices.GetUninitializedObject. Это насколько мне известно.


              1. mayorovp
                07.08.2018 19:52

                Вы все думаете в контексте одного потока. Но их у вас несколько.

                А другой поток может увидеть как сначала был добавлен объект в хеш-таблицу, а потом уже у него был вызван конструктор.


                1. Makeman Автор
                  07.08.2018 20:09
                  -1

                  Пока не отработал конструктор объект никуда не может быть добавлен, поскольку на него ещё нигде нет внешних ссылок.


                  1. mayorovp
                    07.08.2018 20:10

                    Третий раз повторяю: «не будет добавлен» и «ни один поток не увидит его добавленным» — две большие разницы.


                  1. retran
                    07.08.2018 20:21
                    +3

                    У вашего процессора несколько ядер, каждое со своим кэшом и своей личной копией кусочка памяти. Синхронизация кэшей происходит тоже кусочками и совсем не в том порядке, в котором вы что-то пишете в память. Соответственно, одно ядро может увидеть изменения в памяти не в том порядке, в котором они были сделаны другим ядром. Это если не учитывать ещё того, что компилятор может «немного» переписать ваш код и поменять порядок инструкций чтения/записи.


                    1. Makeman Автор
                      07.08.2018 20:29

                      Чтобы воспроизвести такое нужны примеры намного похитрее, чем наш. :)


                      1. mayorovp
                        07.08.2018 20:32

                        Уточнение: чтобы надежно воспроизвести. А вот случайно оно и на вашем примере однажды выплывет. Ночью на выходных, как уже тут писали в комментариях.


                      1. retran
                        07.08.2018 20:48

                        Все гораздо проще. Вот максимально простой аналог того, что происходит у вас — gist.github.com/retran/fa8c6b6671f0c91091a986a22d0f528b


                        1. Makeman Автор
                          08.08.2018 10:50
                          -1

                          Пример интересный, но я не вижу аналогии с текущим случаем. Словарь внутри себя не дожидается выполнения потоков и не делает предположений, о значениях переменных. Грубо говоря, это просто массив, который может увеличиваться, сохраняя индексы элементов, с ненадёжным параллельным чтением (один поток может упустить изменения, только что внесённые другим).


                          1. retran
                            08.08.2018 11:19

                            Там таких случаев вагон по всему коду словаря.

                            Например:
                            github.com/Microsoft/referencesource/blob/60a4f8b853f60a424e36c7bf60f9b5b5f1973ed1/mscorlib/system/collections/generic/dictionary.cs#L463

                            Что будет, если до читающего потока доедет только одно из этих двух присвоений? volatile и барьеров я там не вижу.


                            1. Makeman Автор
                              08.08.2018 11:38

                              Теперь убедили, потенциально тут может возникнуть ArgumentOutOfRangeException.


              1. MaxKot
                08.08.2018 22:06
                +2

                Ссылка на экземпляр объекта становится в первую очередь доступной в конструкторе, а потом уже вовне (если мы её не передали куда-то до завершения выполнения конструктора из самого конструктора).

                Насколько я понимаю, это не так. Сначала выделяется память для объекта, получается ссылка на неициализированный объект. Эта ссылка передаётся в вызов конструктора. Эта же ссылка используется в методе. И, если специально об этом не позаботиться, то гарантий, что ссылка не будет никуда сохранена до инициализации объекта нет!


                Подробнее можно посмотреть в CLR via C# Рихтера и серии статей про модель памяти C#:
                https://msdn.microsoft.com/magazine/jj863136


                Because the BoxedInt instance was incorrectly published (through a non-volatile field, _box), the thread that calls Print may observe a partially constructed object!


                1. Makeman Автор
                  08.08.2018 22:45

                  Спасибо, интересный пример, он очень напоминает передачу ссылки вовне из конструктора.

                  class Tester
                  {
                    BoxedInt2 _box = null;
                    public void Set() {
                      _box = new BoxedInt2();
                    }
                    public void Print() {
                      var b = _box;
                      if (b != null) b.PrintValue();
                    }
                  }

                  По идее, можно исправить так (если компилятор не соптимизирует)
                  class Tester
                  {
                    BoxedInt2 _box = null;
                    public void Set() {
                      var tmp = new BoxedInt2();
                      _box = tmp;
                    }
                    public void Print() {
                      var b = _box;
                      if (b != null) b.PrintValue();
                    }
                  }

                  Поскольку вызов конструктора — блокирующая операция.


                  1. lair
                    08.08.2018 22:49

                    По идее, можно исправить так (если компилятор не соптимизирует)

                    В том-то и дело, что нельзя так ничего исправить, потому что вы ничего не знаете про решения, которые будет принимать компилятор и JIT.


                1. Makeman Автор
                  08.08.2018 23:21

                  Но у меня не получилось воспроизвести ситуацию с недоинициализацией…

                  using System;
                  using System.Threading;
                  using static System.Console;
                  
                  namespace Ace.Base.Console
                  {
                  	class MyClass
                  	{
                  		public static MyClass Instance;
                  
                  		private MyClass() => Thread.Sleep(5000);
                  
                  		public static void AsyncInit() => new Thread(() =>
                  		{
                  			WriteLine("Started");
                  			Instance = new MyClass();
                  		}).Start();
                  	}
                  
                  	static class Program
                  	{
                  
                  		static void Main(string[] args)
                  		{
                  			try
                  			{
                  				MyClass.AsyncInit();
                  				Thread.Sleep(1000);
                  				WriteLine("Ready");
                  				WriteLine(MyClass.Instance?.ToString() ?? "<null>");
                  			}
                  			catch (Exception e)
                  			{
                  				WriteLine(e);
                  				ReadKey(true);
                  			}
                  		}
                  	}
                  }


      1. lair
        07.08.2018 17:50

        Первое правило многопоточности конкурентности: максимально использовать готовые компоненты.


        1. Makeman Автор
          07.08.2018 18:14
          -2

          lock и Dictionary уже готовые, поэтому использую их по максимуму. :)
          За надёжность не ручаюсь, но выглядит работоспособно, мне было бы интересно словить ошибку в такой комбинации, если она возможна.

          Для большей уверенности, конечно, можно использовать ConcurrentDictionary, но его производительность я не измерял собственноручно.


          1. lair
            07.08.2018 18:16
            +1

            За надёжность не ручаюсь, но выглядит работоспособно

            В конкурентных сценариях много что "выглядит" работоспособно, вот только потом падает.


            мне было бы интересно словить ошибку в такой комбинации, если она возможна.

            Она, очевидно, возможна, потому что ничто в вашем коде не гарантирует вас от одновременного выполнения TryGetValue и [x] = y, а эти операции не взаимобезопасны.


      1. mayorovp
        07.08.2018 19:55

        Если в разных потоках создать два делегата от одного метода, то они будут равны, поэтому lock сработает корректно.

        Равны-то равны, но для lock требуется идентичность. А ее запросто может и не быть.


        1. lair
          07.08.2018 20:06
          +1

          В данном конкретном случае ее точно не будет, ибо замыкание. Так что можно считать, что лока нет.


        1. Makeman Автор
          07.08.2018 20:40
          -1

          Её, как будто, гарантируют CLR и компилятор при инициализации статической переменной

              [CompilerGenerated]
              private sealed class <>c
              {
                  public static readonly <>c <>9 = new <>c();
          

          (декомпиляция)


          1. mayorovp
            07.08.2018 20:48
            +2

            Во-первых, нет. Компилятор ничего не гарантирует: сегодня он создает это поле, завтра — уже нет.

            Во-вторых, попробуйте декомпилировать свой код, а не какой-то тестовый. Если так не можете сообразить что замыкание с захваченными переменными не может быть сохранено в статическую переменную по построению.


            1. Makeman Автор
              08.08.2018 11:01

              Да, признаю, в этом предположении я оказался не прав. Нужно задавать более надёжный контекст для lock'а. Пример подправлю.


      1. EviLOne
        08.08.2018 11:01
        +2

        Использование конструкций «довольно безопасное» или «крайне маловероятное» уже не комильфо. Данный подход если не сейчас, то в будущем обязательно приведет к малоприятным событиям. То что провели эксперимент, вы молодец, но смысла в нем не вижу, мало того, это даже опасно для неокрепших умов.


        1. Makeman Автор
          08.08.2018 11:07
          -1

          Моя цель — поделиться идеями, подвергуть их критике и приблизиться истине. Вот с примененим лока на делегате уже нашли изъян, и я признаю, что оказался не прав. :)

          Думаю, это заставляет работать умы людей и более глубоко разбираться в вопросах.


  1. AxisPod
    07.08.2018 14:36

    Хм, неужели открыли type traits в C#. Видимо я пришёл с C++ и подобное использовал сразу, как занялся оптимизациями, даже не подозревал, что для мира C# это не так очевидно.


    1. Makeman Автор
      07.08.2018 17:48
      -1

      Насколько понял из беглого ознакомления с type traits, идеи в основе схожие, но раньше мне не попадалось подобного рода оптимизаций на C#, разве что кэширование в переменную встречал (вместо многократного повторения вызова typeof(T).GetSomething()).

      Статический же TypeOf позволяет несколько обобщить подход, например, реализовать быстрый доступ к информации о типе из разных частей приложения, что довольно удобно, на мой взгляд.


  1. mayorovp
    07.08.2018 14:42
    +2

    Вот еще заметил. Судя по бенчмарку, GetRipeType всегда медленнее чем простой GetType. Так зачем оно нужно?


    1. lair
      07.08.2018 15:54
      +1

      Более того, "типа-паттерн" c TypeOf<T> на Core и Mono медленнее, чем платформенное решение, а на CLR ошибка измерения больше, чем значение.


      (для получения собственно информации о типе)


    1. Makeman Автор
      07.08.2018 17:18

      Вот еще заметил. Судя по бенчмарку, GetRipeType всегда медленнее чем простой GetType. Так зачем оно нужно?

      Зависит от сценария использования. Если получается единожды закэшировать информацию о типе объекта, то потом быстрее её брать из RipeType, чем из Type

      static RipeType AnyRipeType = anyObject.GetRipeType();
      
      static void AnyPerformanceCriticalMethod()
      {
      	/* ... using of AnyRipeType ... */
      }

      В каждом конкретном случае нужно выбирать более оптимальное решение, чтобы достичь максимальной производительности, поскольку есть различия даже на разных CLR.


  1. anonymous
    07.08.2018 23:25
    +1

    Есть еще один распространенный и интересный case:
    typeof(SomeType) == someInstance.GetType()
    Такой код очень хорошо понимает компилятор и хорошо оптимизирует, фактически заменяет на TypeHandle == TypeHandle, что в итоге превращается в небольшое число процессорных инструкций.

    Предложенное Вами решение показывает себя хуже в этом распространенном сценарии.

    static private object stringObject = "";
    
    // ...
    
    [Benchmark] public bool typeof_string_Equals() => typeof(string) == stringObject.GetType();
    [Benchmark] public bool typeof_string_Is() => stringObject is string;
    [Benchmark] public bool TypeOf_string_Equals() => TypeOf<string>.Raw == stringObject.GetType();
    


    typeof_string_Equals: 1.533 ns
    typeof_string_Is: 1.759 ns
    TypeOf_string_Equals: 5.251 ns

    И, да, зря Вы спорите насчет потокобезопасности Dictionary. Он не просто потокоНЕбезопасен, он не всегда работает просто инвалидно с точки зрения данных — бывает он намертво вешает поток, который заходит за чтением, если другой поток зашел за модификацией.


    1. An70ni
      08.08.2018 11:09

      Предложенное Вами решение показывает себя хуже в этом распространенном сценарии.

      а если в цикле вызывать много раз?


      1. lair
        08.08.2018 12:12

        Вы на результаты бенчмарка смотрите.


      1. retran
        08.08.2018 19:00

        Там «либа Акиньшина» используется, она и так гоняет все в цикле много раз.


    1. Makeman Автор
      08.08.2018 11:12
      -1

      Да, каждый конкретный случай лучше рассматривать отдельно и выявлять наболее оптимальное решение, также стоит производить замеры на различных CLR, поскольку результаты иногда сильно плавают.


  1. lair
    08.08.2018 17:54
    +1

    Кстати, по сути поста.


    В этой статье представлены паттерны, позволяющие существенно повысить производительность множественных рефлексивных вызовов посредством техники виртуализации (кэширования) результатов.

    Memoization


    1. retran
      08.08.2018 18:55

      Вот, кстати, было бы интересно сравнить производительность мемоизации на статической переменной и на замыканиях.


      1. Makeman Автор
        08.08.2018 19:36
        -1

        С удовольствием ознакомлюсь с результатами, если вы этим займётесь и поделитесь с остальными. :)