В этой статье представлены паттерны, позволяющие существенно повысить производительность множественных рефлексивных вызовов посредством техники виртуализации (кэширования) результатов.
![image](https://habrastorage.org/getpro/habr/post_images/cb4/bec/1cd/cb4bec1cde8e695b2b449a4dfb179668.png)
Рассмотрим следующий метод:
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();
}
Самое время для бенчмарков!
[
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;
}
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 |
[
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;
}
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 |
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);
}
}
}
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)
AxisPod
07.08.2018 14:36Хм, неужели открыли type traits в C#. Видимо я пришёл с C++ и подобное использовал сразу, как занялся оптимизациями, даже не подозревал, что для мира C# это не так очевидно.
Makeman Автор
07.08.2018 17:48-1Насколько понял из беглого ознакомления с type traits, идеи в основе схожие, но раньше мне не попадалось подобного рода оптимизаций на C#, разве что кэширование в переменную встречал (вместо многократного повторения вызова
typeof(T).GetSomething())
.
Статический же TypeOf позволяет несколько обобщить подход, например, реализовать быстрый доступ к информации о типе из разных частей приложения, что довольно удобно, на мой взгляд.
mayorovp
07.08.2018 14:42+2Вот еще заметил. Судя по бенчмарку, GetRipeType всегда медленнее чем простой GetType. Так зачем оно нужно?
lair
07.08.2018 15:54+1Более того, "типа-паттерн" c
TypeOf<T>
на Core и Mono медленнее, чем платформенное решение, а на CLR ошибка измерения больше, чем значение.
(для получения собственно информации о типе)
Makeman Автор
07.08.2018 17:18Вот еще заметил. Судя по бенчмарку, GetRipeType всегда медленнее чем простой GetType. Так зачем оно нужно?
Зависит от сценария использования. Если получается единожды закэшировать информацию о типе объекта, то потом быстрее её брать из RipeType, чем из Type
static RipeType AnyRipeType = anyObject.GetRipeType(); static void AnyPerformanceCriticalMethod() { /* ... using of AnyRipeType ... */ }
В каждом конкретном случае нужно выбирать более оптимальное решение, чтобы достичь максимальной производительности, поскольку есть различия даже на разных CLR.
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. Он не просто потокоНЕбезопасен, он не всегда работает просто инвалидно с точки зрения данных — бывает он намертво вешает поток, который заходит за чтением, если другой поток зашел за модификацией.Makeman Автор
08.08.2018 11:12-1Да, каждый конкретный случай лучше рассматривать отдельно и выявлять наболее оптимальное решение, также стоит производить замеры на различных CLR, поскольку результаты иногда сильно плавают.
lair
08.08.2018 17:54+1Кстати, по сути поста.
В этой статье представлены паттерны, позволяющие существенно повысить производительность множественных рефлексивных вызовов посредством техники виртуализации (кэширования) результатов.
mayorovp
Dictionary<Type, RipeType>
— непотокобезопасный контейнер. Нельзя его в одном потоке читать, а в другом в то же самое время писать! Да и завязываться на то что компилятор закеширует передаваемый вLock.Invoke
делегат тоже не стоит...Почему бы не использовать ConcurrentDictionary вместо велосипеда?
Makeman Автор
Можно и
ConcurrentDictionary
использовать, если нужно.По началу у меня самого были подозрения насчёт такого решения, но при более детальном анализе я пришёл к выводу, что оно довольно безопасное. Буду признателен, если вы всё же укажете на возможный сценарий, приводящий к ошибке… Мне самому интересно о нём узнать, если он существует.
Если в разных потоках создать два делегата от одного метода, то они будут равны, поэтому
lock
сработает корректно.mayorovp
Пока один поток меняет словарь, второй из него читает и получает мусорные данные. Например, не до конца заполненный RipeType. Или падает с NPE из-за видимого нарушения внутренней структуры словаря.
Makeman Автор
Внешняя ссылка на экземпляр класса RipeType может появится только после выполнения конструктора, в каком бы потоке мы ни выполняли оператор new. Исключение составляет лишь случай вроде
Но это не наша ситуация, поэтому вариант с недоинициализированным RipeType отпадает.
По логике вещей, при чтении структура словаря не может быть нарушена, даже если оно идёт из разных потоков. При параллельной записи тоже, поскольку есть lock. Остаётся лишь случай чтения в момент записи… Мне думается, что словарь не бросит исключение от такого, а если вдруг чтение произошло до момента вставки только что созданного экземпляра и вернулся null, то мы направляемся в lock и дожидаемся завершения вставки, после чего повторяем чтение и получаем уже созданный экземпляр.
retran
Вы же понимаете, что чтение/запись в хэш-таблицу — это не атомарные операции?
З. Ы. Блокировка на делегате — это совсем жесть, конечно.
Makeman Автор
Понимаю, конечно. Но в данном решении атомарность и не требуется за счёт повторного чтения под локом.
С непривычки да, но как бы должно работать, поскольку компилятор обеспечивает однозначный и потокобезопасный контекст блокировки.
lair
То есть то, что у вас чтение бывает для разных ключей, вы и забыли, да?
Makeman Автор
Повторное чтение происходит по тому же ключу, что и при первой неудачной попытке.
retran
Что произойдет при первом чтении по двум разным ключам «одновременно» из двух потоков?
Боттлнек на пустом месте, да.
lair
… вот именно поэтому не надо придумывать свою реализацию, не разобравшись в проблеме.
Вот у вас есть словарь, в нем есть значение для ключа A. Теперь к вам одновременно пришли запросы для ключей A и B. Первый попадет в чтение, второй — в запись, и они могут идти строго одновременно, потому что на первый не распространяется лок.
lair
Не, не должно. У вас каждый раз при входе в метод будет создаваться новый экземпляр скрытого типа, и поэтому
lock
всегда будет получать новый объект.lair
Не, не бросит. Просто тихо вернет не то значение. Вы внутрь
TryGetValue
никогда не заглядывали?Makeman Автор
Заглядывал когда-то давно. Конечно, могу ошибаться, но на вскидку такое маловероятно, поскольку если не найдено соответствия по ключу, то с чего бы возвращать дугое значение. Теоретически, может быть такое, что ключ уже попал в словарь, а значение пока ещё не присвоилось, но тогда хотя бы дефолтный null вернуться должен.
Я прямо говорю, что мной выбрано такое решение в целях эксперимента и обсуждения, поскольку есть подозрение, что оно рабочее.
retran
Там внутри таблица связных списков поверх массива с ресайзом этого самого массива, в ходе которого элементы могут быть заново переразложены по спискам с пересчетом хэшкодов со случайной солью. То есть, никто не гарантирует, что при пересчете элементы хэш-таблицы «случайно» не обменяются хэшкодами.
UPD github.com/Microsoft/referencesource/blob/60a4f8b853f60a424e36c7bf60f9b5b5f1973ed1/mscorlib/system/collections/generic/dictionary.cs#L386
Makeman Автор
Обменяться-то они могут, но по хэш-коду вычисляется номер «корзины» (связного списка), а поиск в списке уже идёт по строгой эквивалентности ключа, поэтому в худшем случае элемент может не найтись, хотя он в словаре присутствует (и-то мне видится это крайне маловероятным событием, если вообще возможным).
retran
Только вот корзина — это связный список на индексах. И все корзины лежат в одном массиве, а индексы у элементов меняются, причем перемещение элемента неатомарное и включает, кажется, с полдесятка операций записи и зависит не то чтобы от конкретного рантайма, а от конкретной его сборки.
Построить полную картину того, что может произойти при контеншне не берусь, там сотни вариантов с разным исходом, в том числе возврате любого произвольного элемента вместо null.
Самый простой вам ниже lair показал.
leotsarev
Я например видел такое маловероятное событие на проде два раза. Один раз ночью в выходной
lair
А вы загляните.
Вот если после
FindEntry
массивentries
поменяет размер (с перекладкой всего), а именно это происходит (иногда) при добавлении новой записи, значение по индексу (entries[entry]
) будет совсем не от нужной записи.Неправильное подозрение.
Dictionary<K,V>
нельзя использовать в сценариях конкурентного чтения/записи без явной блокировки всех операций.lair
… на самом деле, немного сложнее, потому что
entries
в той реализации, на которую я смотрю, никогда не перемешивается. Зато внутриFindEntry
есть прекрасный код, смотрит наthis.buckets[num % this.buckets.Length]
и в зависимости от настроения оптимизатора в этом месте можно получить что угодно, включая погоду на Марсе, например, когда между обращением кbuckets.Length
и обращением кbuckets[x]
содержимоеbuckets
поменялось — а вотbuckets
точно перемешивается.Makeman Автор
Это, конечно, интересный момент, но по беглому изучению кода выглядит так, что entries при добавлении новых элементов может лишь увеличиваться в размере, а перекладка элементов в новый массив происходит без смешивания, через Array.Copy, поэтому даже старый индекс будет валиден в случае нового массива, вопрос остаётся открытым…
lair
https://habr.com/post/419511/#comment_18968353
Makeman Автор
Выглядит так, что в худшем случае мы можем лишь потерять элемент, уже находящийся в словаре, что приведёт к его пересозданию извне, но если элемент по ключу найден, то чтение безопасно, поскольку индеск в массиве за ним закрепляется навсегда.
lair
… а поскольку пересоздание внутри себя тоже не потокобезопасно, там можно получить исключение. И это не единственный возможный сценарий.
Makeman Автор
Насчёт пересоздания тоже ещё вопрос, но в нашем случае даже при таком неудачном раскладе исключения точно не будет, просто создастся новый экземпляр и по ключу заменит старый в словаре.
lair
Вы проверил все возможные сценарии, со всеми расположениями переменных? Например, что случится, если у вас происходит два одновременных добавления, и два потока одновременно заберут ссылку на следующий свободный элемент, и там окажется хэш-код и ключ от одного элемента, а значение — от другого?
Makeman Автор
Нет, я не проверял все возможные сценарии и отталкиваюсь лишь от того, что TryGetValue возвращает true и сам элемент, если он присутствует в словаре по ключу, либо false если его там нет или он только что асинхронно добавлен в процессе чтения другим потоком (и на этот случай выполняется повторное чтение в критической секции).
lair
Вы, повторюсь, забыли, что у вас параллельно еще присвоения идут?
Makeman Автор
Запись элементов идёт только под lock'ом. То есть я допускаю лишь ситуацию с ненадёжным параллельным чтением, которая обрабатывается под тем же lock'ом.
lair
… который у вас долгое время не работал. Кстати, про syncroot на коллекциях вы не знаете, да?
Потенциально возвращенный null вы тоже обрабатываете? Что-то не видно.
Makeman Автор
Знаю, но в некоторых случаях мне хотелось бы абстрагироваться от введения явной переменной, отчего и появился
К сожалению, как верно указали, в случае замыканий переменных он работать не будет.
Обрабатывается потенциально возвращённый false. :)
lair
… поэтому вы ввели дополнительное поле вместо использование существующего.
Кстати, ваш словарь еще и публичный, поэтому все и любые утверждения про то, что модификация только под локом, невалидны.
Вот только вы можете получить
true
иnull
.Makeman Автор
Спасибо, в коде я это уже подправил, а в статье осталась неточность, подкорректирую.
Может быть, и могу. Но дело в том, что у меня такая позиция в программировании — испытывать на прочность самые неожиданные сценарии и варианты, а не ходить по проторенным и безопасным тропинкам. :)
Конечно, в коммерческих рабочих проектах я обычно применяю более надёжные решения, но в своих исследовательских ни в чём таком себя не сдерживаю.
lair
… а потом доказывать, что они безопасные. Спасибо, но нет.
(и нет, вы ничего не испытываете, потому что вы не видите никаких проблем в вашем коде, пока вам на них не покажут)
Makeman Автор
Я дискутирую, а не доказываю и, как видите, при наличии убедительных аргументов, готов признавать свои ошибки.
Во-первых, даже опытные разработчики могут не увидеть проблемы в своём коде, которая выльется в виде бага.
Во-вторых, некоторые подозрительные места я замечаю и мне наоборот весьма интересно провеить их в нестандартных условиях.
Для меня подобные публикации что-то вроде код-ревью от сообщества, с чем-то соглашаюсь, с чем-то нет.
lair
Как мы уже неоднократно наблюдали (и обсуждали), у вас очень удобное для вас понимание убедительных аргументов.
Что-то этой проверки не видно в посте.
Я и говорю: вы выбираете то, что вам хочется. Правильность или ее отсутствие вас не волнуют.
Makeman Автор
В конце концов каждый выбирает то, что ему хочется и видится правильным. По некоторым вопросам я до сих пор придерживась мнения отличающегося от вашего, и это нормально иметь разные мнения.
lair
… если бы вы были готовы признавать свои ошибки, в вашем коде уже давно был бы
ConcurrentDictionary
илиImmutableDictionary
. Но вы продолжаете костылить вокруг обычного.Makeman Автор
Дело в том, что часть тестов производительности проведена на обычном словаре, поэтому я не хочу сейчас менять имплементацию и, соответственно, результаты тестов.
Всё предоставлено как есть, каждый сам может внести требуемые правки в реализацию при необходимости и проверить производительность.
lair
Который не подходит для этой задачи, и, значит, ваши тесты невалидны.
Makeman Автор
С чего вы взяли? Может, у кого-то однопоточное приложение и ему вполне хватит такого словаря.
lair
Может, у кого-то приложение, в котором нет обращения к
GetType
в цикле, и ему не нужна мемоизация.Вы сравниваете решение, которое корректно работает всегда, с решением, которое работает иногда, и никак это не оговариваете. Некрасиво.
Makeman Автор
Чтобы уж не было никаких сомнений, укажу, что это может повлиять на производительность.
lair
Это как раз к разговору о ваших гипотезах. Вы даже не тестировали производительность в многопоточных режимах.
И какой тогда смысл в проведенных вами тестах, если половину кода под ними использовать нельзя?
Makeman Автор
А другую половину можно.
lair
Угу, и какая есть какая, надо догадываться самостоятельно.
Или вот, скажем, вы производительность посчитали, а потребление памяти — нет. А ведь это стандартная оборотная сторона мемоизации (и особенно это интересно для generic-типов, ага). Или вот, скажем, у вас нигде нет оценки, начиная с какого момента накладные расходы на стоимость инициализации перестают превышать выигрыш от кэширования (и что делать, если мне не нужна кэшируемая информация).
Makeman Автор
Скажу прямо — если бы кто-то мне платил за то время и силы, что я трачу на статьи, то можно было бы говорить о разжёвывании материала, подробном анализе и детальном рассмотрении всех возможных аспектов.
Сейчас я делаю это as is — указываю на ключевые моменты и идеи, предоставляю примеры, а читатель уже сам решит, что и как ему с этим делать. Я ничего не продаю и не рекламирую, если и публикую ссылки на код, то лишь делюсь личными наработками с другими людьми, и да, иногда кто-то находит там для себя что-то интересное.
lair
Это, конечно, повод, когда вам не платят за ваше время и силы, публиковать посредственные статьи с плохим анализом.
… и о качестве этих наработок, я полагаю из сказанного выше, можно сказать то же самое, что и о качестве этой статьи.
Makeman Автор
В этой статье я не занимаюсь анализом, а всего лишь, как сказано ранее, выдвигаю гипотезу, провожу эксперименты и делюсь результатами — в этом моя цель.
И одна из причин, почему не углубляюсь в анализ, это те дикие дебри, которые лежат за полученными данными, почему они именно такие…
И да, в наработках много экспериментальных идей, которые со временем могут выбраковываться из-за их несостоятельности, это вполне нормальный процесс. Есть и такие, что остаются.
lair
Какой смысл в результатах (неправильно проведенного эксперимента) без анализа?
Кстати, а как же вы делаете выводы (которые в вашей статье есть) без анализа? Просто "что придумывалось"?
Ну то есть вы вывалили нам какие-то результаты, полученные неизвестно из чего, и даже не знаете, что они означают. Круто.
Makeman Автор
Выводы не выходят за рамки ответа на ваш предыдущий вопрос «в чём смысл?».
Раз вы так уверены в моём невежестве, то могли бы сами и пояснить, что они означают…
lair
Вы под "способами ускорения вызовов" понимаете "давайте запишем в поле"? Серьезно?
Как вы можете утверждаеть, что есть способы ускорения, если вы не проводили анализ резульатов?
Вы их сделали, не проводя анализа?
В том-то и дело, что они ничего не означают.
Makeman Автор
Серьёзно. Это просто, но не очевидно.
Замеры производительности на различных бенчмарках о многом говорят. И если вы считаете, что это рандомные значения, не несущие за собой смысла, то, пожалуйста, факты в студию, разрушьте мою иллюзию и раскроте глаза тем, кто в неё тоже начал верить.
lair
Если вам это не было очевидно, то мне очень вас жаль. Для меня способ "оптимизации множественных вызовов" путем записи результата первого из них в переменную известен лет двадцать с лишним.
… и о чем же?
Вы про чайник Рассела никогда не слышали?
Makeman Автор
Извините, конечно, но в дженерик случае через статических класс — это не настолько очевидно, как в обычном. Вы сами-то применяли осознанно такое решение раньше? А если применяли, то на каком году программирования дошли?
Оставляю на ваш суд.Про чайник теперь услышал.
lair
Ну да.
На первом году пользования дженериками, ровно в тот момент, когда понял, что для каждого варианта дженерика в .net создается свой тип.
Смешно, да. Когда я вам говорю, что замеры бессмысленны, вы просите это доказать фактами. А когда я спрашиваю, что они значат, вы оставляете это на мой суд.
Makeman Автор
Здорово, а я вот лет 7 пользовался дженериками и только на седьмом году чётко понял, что кэшировать переменную в дженерик-методе удобно через статический дженерик класс.
Поэтому можете считать, что свои посредственные статьи пишу для таких же тугодумов, как и я сам, если вам так проще.
Возможно, вы уже ушли далеко вперёд в своём профессиональном развитии и для вас все эти замеры выглядят бессмысленно, но со скромных высот моих познаний смысл в них всё же есть.
lair
… а это удобно? Никогда бы не подумал. И код, который вы приводите, традиционно плох. Даже нет, не плох — ужасен.
В таком случае заодно можно считать, что этим статьям не место на хабре, потому что мне хочется думать, что аудитория здесь не состоит из "таких же тугодумов".
Makeman Автор
Вам, может, и хочется так думать, но своих читателей на Хабре публикации находят.
lair
Зачем? Это единственно верная формулировка задачи?
Определите критерии оптимальности "производительности". Оптимальное по времени выполнения? По памяти? По одному с ограничением по другому? Без ограничений? В однопоточном сценарии? Многопоточном? Сколько разных
T
мы ожидаем? Какого размера кэшируемое значение? Какова стоимость создания значения?Makeman Автор
Критерии:
— минимальное время выполнения при многократных вызовах
— серьёзных ограничений по памяти нет, потребление в переделах разумного
— чтение многопоточное
— достаточно одного T
— кэшируемое значение любое (для простоты bool, string, object)
— стоимость создания определяется так: если кэшированный доступ даёт выигрыш по производительности в 2 и более раза в сравнении с созданием, то задача решена
— желательно ещё, чтобы это было справедливо для любой CLR (.NET Framework, .NET Core, Mono).
lair
Так кто вам сказал, что это правильная задача?
У всех разное понимание "разумного".
Вы не поняли вопроса. Чтобы знать, дает ли кэшированный доступ выигрыш, нужно знать три вещи: стоимость доступа к кэшу, количество обращений и стоимость создания значения. Первое я могу померять. Второе и третье — условия задачи.
Makeman Автор
Возможно, я двусмысленно уточнил, но _value должно быть не общим значеним для любых T, а для каждого T конкретным. Под «достаточно одного T» имелось в виду, что у метода один дженерик параметр, а различных значений T пусть будет от 10 до 100.
Количество обращений более 100. Стоимость значения, как у typeof(T).Name/Assembly/IsValueType.
lair
retran
В этот момент я заявляю, что вы страдаете херней и если у вас реально боттлнек в этом месте, то любой «паттерн» проиграет предзаполненному lookup table на Dictionary без блокировок вообще.
Makeman Автор
Чтобы уж точно не было разночтений по стоимости создания, можете просто взять за эталон работу с типами из публикации typeof(T).Name/Assembly/IsValueType.
Нужно сделать быстрее в два раза при множественных вызовах.
retran
Меня в институте учили, что испытание/эксперимент, это:
1. Подробное изучение всей доступной информации об объекте эксперимента.
2. Выдвижение четкой гипотезы, базирующейся на известной информации, а не на предположениях.
3. Разработка и проведение повторяемых экспериментов, в том числе опровергающих гипотезу.
Makeman Автор
Я от этого далеко и не отхожу:
1. изучена работа typeof и Type
2. выдвинута чёткая гипотеза, что TypeOf и RipeType могут работать быстрее в некоторых сценариях
3. разработан и проведён ряд повторяемых экспериментов, в том числе опровергающих гипотезу
Получены результаты и предоставлены на рассмотрение широкому сообществу. :)
lair
"… некоторых сценариях". Очень "четкая" гипотеза.
Makeman Автор
Извините, но за детализацией отправлю вас к публикации, где чётко прописаны все исследуемые сценарии.
lair
Но вы так и не определили, почему оно имеет разную производительность на разных фреймворках.
lair
(Это я даже не начал вдаваться в статистический инструментарий под формулировкой гипотезы и ее проверкой)
Makeman Автор
Вообще-то определил, но, к сожалению, теперь вы не сможете просмотреть детали в открытом доступе, которые раньше находились тут.
В общих словах, различные CLR генерируют неодинаковый код во время JIT-компиляции при доступе к статическим рид-онли полям классов (некоторые добавляют дополнительную проверку на то, проинициализировано ли поле, что сказывается на производительности). Тема также тесно связана с добавлением статических конструкторов, у которых, как оказывается, есть ряд подводных камней…
Здесь вообще разговор для отельной статьи, так что при желании можете сами углубиться в этот вопрос, а потом поделиться результатами с остальными.
lair
Ну да, а публикации на хабре они недостойны, дадада.
И снова вопрос: почему?
Мне-то это зачем? Это вы выдвигаете какие-то гипотезы, а у меня проблемы с производительностью совсем в других местах.
Makeman Автор
На вопрос почему разные CLR генерируют отличающийся код, исчерпывающего ответа не нашёл и в оригинальной дискуссии его тоже не оставили.
lair
Печально.
Makeman Автор
Судя по количеству добавлений публикации в закладки, кто-то всё же считает информацию пусть даже потенциально, но полезной.
An70ni
то время, что было потрачено на этот спор можно было потратить на проведение дополнительных тестов. всяко полезнее.
Makeman Автор
Для проведения дополнительных тестов открыты все исходные коды. Можно модифицировать их по своему усмотрению и проверять различные интересующие сценарии.
Так что любой товарищ, участвующий в споре или наблюдающий за ним со стороны, может их провести.
retran
Только вот проблема с перерасчетом хэшей остаётся.
И мы ещё даже не начали за когерентность кэшей разговаривать.
retran
Хотя, справедливости ради, проверка на полное равенство ключей там тоже есть. Но это не отменяет того, что хэш-таблицы во время ресайза неконсистентна.
lair
Там есть дофига мест, где используется модуль от текущего (меняющегося) размера, так что можно получить много боли. Получить не тот айтем возвращенным так просто не выйдет, но вот нарваться на IndexOutOfRange или Duplicate Key — да.
Makeman Автор
По сути TryGetValue гарантирует, что не будет исключений из какого бы мы потока не работали со словарём. Может только false вернуться при несинхронном добавлении элемента из другого потока (поскольку словарь непотокобезопасный). Этот второй случай обрабатывается повторным чтением в lock.
Мне так видится реализация.
lair
Не гарантирует. В текущей реализации мы не нашли места, где может быть исключение — это да. Но никаких гарантий нет.
Но у вас же и от одновременных присвоений нет никакой защиты.
Makeman Автор
Сама идеология работы метода при правильной имплементации должна гарантировать отсутствие всяких исключений. Если исключения есть, значит, плохо реализован метод, в нём баг.
mayorovp
При правильной имплементации и правильном использовании. Но вы используете его неправильно.
Makeman Автор
Пока убедительных аргументов, почему метод используется неправильно, я не услышал. Исключений нет, как выяснили, другой элемент тоже не придёт в результате. Если даже элемен вдруг потеряется, что крайне маловероятно, то в нашем случае ничего серьёзного не произойдёт, создадим новый вместо прежнего.
Makeman Автор
Возможно, для других сценариев это критично, что накладывает ограничения на применение, но для конкретного допустимо.
mayorovp
Почему для вас «Dictionary — не потокобезопасный класс» не аргумент?
Makeman Автор
Можно два определения дать потокобезопасности:
1. Гаранития того, что коллекция вообще будет работать в условиях нескольких потоков
2. Гарантия того, что при записи/удалении/замене элемента одним потоком, второй изменения сразу же увидит
Сейчас я придерживаюсь второго, более сильного. Словарь, не являясь потокобезопасным классом, способен работать в условиях нескольких потоков, но может давать ненадёжные результаты.
mayorovp
От смены определения Dictionary в условиях нескольких потоков гарантированно работать не начнет.
Makeman Автор
Изначально я придерживаюсь такого определения:
lair
Таких гарантий по отношению к Dictionary никто не дает (ну, если, конечно, вы не считаете периодическое бросание исключений "ненадежным результатом").
lair
Это только в текущей реализации. Завтра ее поменяют — и у вас все упадет (это, если что, говорит человек, у которого именно такое случилось при апгрейде с 4.7.1 на 4.7.2).
Makeman Автор
Любопытно, с чем именно вы столкнулись при апгрейде? Мне просто интересно узнавать такие тонкости в реализациях стандартных классов.
lair
С изменением внутренней реализации EtwTrace для asp.net.
lair
Вы забыли одно важное дополнение: при выполнении предусловий.
Теперь открываем документацию:
Выделенное мное условие у вас не выполняется. Я вам больше того скажу, в общем случае
TryGetValue
может упасть и сейчас — если параллельноClear
вызвать.Makeman Автор
Не вижу причин для падения даже при параллельном Clear.
lair
Плохо смотрите.
1:
2:
lair
А нет, здесь я не прав, там есть проверка на
i >= 0
.lair
… впрочем, это все ровно до тех пор, пока кто-нибудь не прикрутит к словарю тримминг.
Makeman Автор
Да, потенциально (хотя не стопроцентный факт, но для меня убедительный) это место может упасть с ArgumentOutOfRangeException из-за возможной гонки при присваивании в две переменные, как упомянул в комментариях retran.
MaxKot
Малая веротность вызова FormatDisk не делает этот код правильным. Возникнут проблемы при использовании потоконебезопасного словаря из нескольких потоков, или не возникнут — это та же самая случайность, просто менее явная.
Возможно даже, что в ваших проектах эта случайность допустима. Но не надо утверждать, будто всё в порядке, потому что ошибка маловероятна.
Makeman Автор
Я в комментариях признал, что со словарём у меня ненадёжный код и даже внёс соответствующее примечание в публикацию.
mayorovp
Нет, это не так. Другой поток может "увидеть" изменения в памяти не в том порядке в котором они вносились.
Makeman Автор
Поясните…
Ссылка на экземпляр объекта становится в первую очередь доступной в конструкторе, а потом уже вовне (если мы её не передали куда-то до завершения выполнения конструктора из самого конструктора). Исключение составляет случай создания объекта без вызова конструктора FormatterServices.GetUninitializedObject. Это насколько мне известно.
mayorovp
Вы все думаете в контексте одного потока. Но их у вас несколько.
А другой поток может увидеть как сначала был добавлен объект в хеш-таблицу, а потом уже у него был вызван конструктор.
Makeman Автор
Пока не отработал конструктор объект никуда не может быть добавлен, поскольку на него ещё нигде нет внешних ссылок.
mayorovp
Третий раз повторяю: «не будет добавлен» и «ни один поток не увидит его добавленным» — две большие разницы.
retran
У вашего процессора несколько ядер, каждое со своим кэшом и своей личной копией кусочка памяти. Синхронизация кэшей происходит тоже кусочками и совсем не в том порядке, в котором вы что-то пишете в память. Соответственно, одно ядро может увидеть изменения в памяти не в том порядке, в котором они были сделаны другим ядром. Это если не учитывать ещё того, что компилятор может «немного» переписать ваш код и поменять порядок инструкций чтения/записи.
Makeman Автор
Чтобы воспроизвести такое нужны примеры намного похитрее, чем наш. :)
mayorovp
Уточнение: чтобы надежно воспроизвести. А вот случайно оно и на вашем примере однажды выплывет. Ночью на выходных, как уже тут писали в комментариях.
retran
Все гораздо проще. Вот максимально простой аналог того, что происходит у вас — gist.github.com/retran/fa8c6b6671f0c91091a986a22d0f528b
Makeman Автор
Пример интересный, но я не вижу аналогии с текущим случаем. Словарь внутри себя не дожидается выполнения потоков и не делает предположений, о значениях переменных. Грубо говоря, это просто массив, который может увеличиваться, сохраняя индексы элементов, с ненадёжным параллельным чтением (один поток может упустить изменения, только что внесённые другим).
retran
Там таких случаев вагон по всему коду словаря.
Например:
github.com/Microsoft/referencesource/blob/60a4f8b853f60a424e36c7bf60f9b5b5f1973ed1/mscorlib/system/collections/generic/dictionary.cs#L463
Что будет, если до читающего потока доедет только одно из этих двух присвоений? volatile и барьеров я там не вижу.
Makeman Автор
Теперь убедили, потенциально тут может возникнуть ArgumentOutOfRangeException.
MaxKot
Насколько я понимаю, это не так. Сначала выделяется память для объекта, получается ссылка на неициализированный объект. Эта ссылка передаётся в вызов конструктора. Эта же ссылка используется в методе. И, если специально об этом не позаботиться, то гарантий, что ссылка не будет никуда сохранена до инициализации объекта нет!
Подробнее можно посмотреть в CLR via C# Рихтера и серии статей про модель памяти C#:
https://msdn.microsoft.com/magazine/jj863136
Makeman Автор
Спасибо, интересный пример, он очень напоминает передачу ссылки вовне из конструктора.
По идее, можно исправить так (если компилятор не соптимизирует)
Поскольку вызов конструктора — блокирующая операция.
lair
В том-то и дело, что нельзя так ничего исправить, потому что вы ничего не знаете про решения, которые будет принимать компилятор и JIT.
Makeman Автор
Но у меня не получилось воспроизвести ситуацию с недоинициализацией…
lair
Первое правило
многопоточностиконкурентности: максимально использовать готовые компоненты.Makeman Автор
lock и Dictionary уже готовые, поэтому использую их по максимуму. :)
За надёжность не ручаюсь, но выглядит работоспособно, мне было бы интересно словить ошибку в такой комбинации, если она возможна.
Для большей уверенности, конечно, можно использовать ConcurrentDictionary, но его производительность я не измерял собственноручно.
lair
В конкурентных сценариях много что "выглядит" работоспособно, вот только потом падает.
Она, очевидно, возможна, потому что ничто в вашем коде не гарантирует вас от одновременного выполнения
TryGetValue
и[x] = y
, а эти операции не взаимобезопасны.mayorovp
Равны-то равны, но для lock требуется идентичность. А ее запросто может и не быть.
lair
В данном конкретном случае ее точно не будет, ибо замыкание. Так что можно считать, что лока нет.
Makeman Автор
Её, как будто, гарантируют CLR и компилятор при инициализации статической переменной
(декомпиляция)
mayorovp
Во-первых, нет. Компилятор ничего не гарантирует: сегодня он создает это поле, завтра — уже нет.
Во-вторых, попробуйте декомпилировать свой код, а не какой-то тестовый. Если так не можете сообразить что замыкание с захваченными переменными не может быть сохранено в статическую переменную по построению.
Makeman Автор
Да, признаю, в этом предположении я оказался не прав. Нужно задавать более надёжный контекст для lock'а. Пример подправлю.
EviLOne
Использование конструкций «довольно безопасное» или «крайне маловероятное» уже не комильфо. Данный подход если не сейчас, то в будущем обязательно приведет к малоприятным событиям. То что провели эксперимент, вы молодец, но смысла в нем не вижу, мало того, это даже опасно для неокрепших умов.
Makeman Автор
Моя цель — поделиться идеями, подвергуть их критике и приблизиться истине. Вот с примененим лока на делегате уже нашли изъян, и я признаю, что оказался не прав. :)
Думаю, это заставляет работать умы людей и более глубоко разбираться в вопросах.