Skip to content

Commit 54614c0

Browse files
committed
Reduced allocation for trivial cases
1 parent 5fcf4e5 commit 54614c0

File tree

3 files changed

+83
-20
lines changed

3 files changed

+83
-20
lines changed

src/DotNext.Tests/Threading/CancellationTokenMultiplexerTests.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public static void CanceledImmediately()
99
using var scope = multiplexer.Combine([new(true), new(true)]);
1010
True(scope.Token.IsCancellationRequested);
1111
Equal(new(true), scope.CancellationOrigin);
12-
NotEqual(new(true), scope.Token);
12+
Equal(new(true), scope.Token);
1313
}
1414

1515
[Fact]
@@ -19,21 +19,22 @@ public static async Task CanceledImmediatelyAsync()
1919
await using var scope = multiplexer.Combine([new(true), new(true)]);
2020
True(scope.Token.IsCancellationRequested);
2121
Equal(new(true), scope.CancellationOrigin);
22-
NotEqual(new(true), scope.Token);
22+
Equal(new(true), scope.Token);
2323
}
2424

2525
[Fact]
2626
public static void CheckPooling()
2727
{
28+
using var cts = new CancellationTokenSource();
2829
CancellationToken token;
2930
var multiplexer = new CancellationTokenMultiplexer { MaximumRetained = int.MaxValue };
30-
using (var scope = multiplexer.Combine([new(false)]))
31+
using (var scope = multiplexer.Combine([cts.Token, cts.Token, cts.Token]))
3132
{
3233
token = scope.Token;
3334
}
3435

3536
// rent again
36-
using (var scope = multiplexer.Combine([new(false)]))
37+
using (var scope = multiplexer.Combine([cts.Token, cts.Token, cts.Token]))
3738
{
3839
Equal(token, scope.Token);
3940
}

src/DotNext.Threading/Threading/CancellationTokenMultiplexer.Scope.cs

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
using System.Diagnostics;
2+
using System.Runtime.CompilerServices;
13
using System.Runtime.InteropServices;
24

35
namespace DotNext.Threading;
46

7+
using Runtime;
8+
59
partial class CancellationTokenMultiplexer
610
{
711
/// <summary>
@@ -10,44 +14,90 @@ partial class CancellationTokenMultiplexer
1014
[StructLayout(LayoutKind.Auto)]
1115
public readonly struct Scope : IMultiplexedCancellationTokenSource, IDisposable, IAsyncDisposable
1216
{
13-
private readonly CancellationTokenMultiplexer multiplexer;
14-
private readonly PooledCancellationTokenSource source;
17+
// CancellationToken is just a wrapper over CancellationTokenSource.
18+
// For optimization purposes, if only one token is passed to the scope, we can inline the underlying CTS
19+
// to this structure.
20+
private readonly ValueTuple<object> multiplexerOrToken;
21+
private readonly PooledCancellationTokenSource? source;
1522

1623
internal Scope(CancellationTokenMultiplexer multiplexer, ReadOnlySpan<CancellationToken> tokens)
1724
{
18-
this.multiplexer = multiplexer;
19-
source = multiplexer.Rent();
20-
21-
foreach (var token in tokens)
25+
switch (tokens)
2226
{
23-
source.Add(token);
27+
case []:
28+
source = null;
29+
multiplexerOrToken = InlineToken(new(canceled: false));
30+
break;
31+
case [var token]:
32+
source = null;
33+
multiplexerOrToken = InlineToken(token);
34+
break;
35+
case [var token1, var token2]:
36+
source = null;
37+
if (!token1.CanBeCanceled || token1 == token2)
38+
{
39+
multiplexerOrToken = InlineToken(token2);
40+
}
41+
else if (!token2.CanBeCanceled)
42+
{
43+
multiplexerOrToken = InlineToken(token1);
44+
}
45+
else
46+
{
47+
goto default;
48+
}
49+
50+
break;
51+
default:
52+
multiplexerOrToken = new(multiplexer);
53+
source = multiplexer.Rent(tokens);
54+
break;
2455
}
2556
}
2657

58+
private static ValueTuple<object> InlineToken(CancellationToken token)
59+
=> CanInlineToken ? Unsafe.BitCast<CancellationToken, ValueTuple<object>>(token) : new(token);
60+
61+
private static CancellationToken GetToken(ValueTuple<object> value)
62+
=> CanInlineToken ? Unsafe.BitCast<ValueTuple<object>, CancellationToken>(value) : (CancellationToken)value.Item1;
63+
64+
// This property checks whether the reinterpret cast CancellationToken => CancellationTokenSource
65+
// is safe. If not, just box the token.
66+
private static bool CanInlineToken => Intrinsics.AreCompatible<CancellationToken, ValueTuple<object>>()
67+
&& RuntimeHelpers.IsReferenceOrContainsReferences<CancellationToken>();
68+
2769
/// <summary>
2870
/// Gets the cancellation token that can be canceled by any of the multiplexed tokens.
2971
/// </summary>
30-
public CancellationToken Token => source.Token;
72+
public CancellationToken Token => source?.Token ?? GetToken(multiplexerOrToken);
3173

3274
/// <summary>
3375
/// Gets the cancellation origin if <see cref="Token"/> is in canceled state.
3476
/// </summary>
35-
public CancellationToken CancellationOrigin => source.CancellationOrigin;
77+
public CancellationToken CancellationOrigin => source?.Token ?? GetToken(multiplexerOrToken);
3678

3779
/// <inheritdoc/>
3880
public void Dispose()
3981
{
40-
for (var i = 0; i < source.Count; i++)
82+
if (source is not null)
4183
{
42-
source[i].Dispose();
43-
}
84+
Debug.Assert(multiplexerOrToken.Item1 is CancellationTokenMultiplexer);
4485

45-
// now we sure that no one can cancel the source concurrently
46-
Return(multiplexer, source);
86+
for (var i = 0; i < source.Count; i++)
87+
{
88+
source[i].Dispose();
89+
}
90+
91+
// now we sure that no one can cancel the source concurrently
92+
Return(Unsafe.As<CancellationTokenMultiplexer>(multiplexerOrToken.Item1), source);
93+
}
4794
}
4895

4996
/// <inheritdoc/>
50-
public ValueTask DisposeAsync() => ReturnAsync(multiplexer, source);
97+
public ValueTask DisposeAsync()
98+
=> source is not null
99+
? ReturnAsync(Unsafe.As<CancellationTokenMultiplexer>(multiplexerOrToken.Item1), source)
100+
: ValueTask.CompletedTask;
51101

52102
private static async ValueTask ReturnAsync(CancellationTokenMultiplexer multiplexer, PooledCancellationTokenSource source)
53103
{

src/DotNext.Threading/Threading/CancellationTokenMultiplexer.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public int MaximumRetained
2929
/// </summary>
3030
/// <param name="tokens">The tokens to be combined.</param>
3131
/// <returns>The scope that contains a single multiplexed token.</returns>
32-
public Scope Combine(ReadOnlySpan<CancellationToken> tokens)
32+
public Scope Combine(ReadOnlySpan<CancellationToken> tokens) // TODO: use params
3333
=> new(this, tokens);
3434

3535
private void Return(PooledCancellationTokenSource source)
@@ -81,4 +81,16 @@ private PooledCancellationTokenSource Rent()
8181

8282
return current;
8383
}
84+
85+
private PooledCancellationTokenSource Rent(ReadOnlySpan<CancellationToken> tokens)
86+
{
87+
var source = Rent();
88+
89+
foreach (var token in tokens)
90+
{
91+
source.Add(token);
92+
}
93+
94+
return source;
95+
}
8496
}

0 commit comments

Comments
 (0)