Skip to content

Commit b19fda9

Browse files
Optimize caching string generation
1 parent 176bc8a commit b19fda9

File tree

4 files changed

+137
-5
lines changed

4 files changed

+137
-5
lines changed

src/ImageSharp.Web/Caching/CacheHash.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public string Create(string value, uint length)
5151
}
5252

5353
[MethodImpl(MethodImplOptions.AggressiveInlining)]
54-
private static string HashValue(string value, uint length, Span<byte> bufferSpan)
54+
private static string HashValue(ReadOnlySpan<char> value, uint length, Span<byte> bufferSpan)
5555
{
5656
using var hashAlgorithm = SHA256.Create();
5757
Encoding.ASCII.GetBytes(value, bufferSpan);

src/ImageSharp.Web/Caching/PhysicalFileSystemCache.cs

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Apache License, Version 2.0.
33

4+
using System;
45
using System.IO;
6+
using System.Runtime.CompilerServices;
7+
using System.Runtime.InteropServices;
58
using System.Threading.Tasks;
69
using Microsoft.AspNetCore.Hosting;
710
using Microsoft.Extensions.FileProviders;
@@ -21,6 +24,11 @@ public class PhysicalFileSystemCache : IImageCache
2124
/// </summary>
2225
private readonly string cacheRootPath;
2326

27+
/// <summary>
28+
/// The length of the filename to use (minus the extension) when storing images in the image cache.
29+
/// </summary>
30+
private readonly int cachedNameLength;
31+
2432
/// <summary>
2533
/// The file provider abstraction.
2634
/// </summary>
@@ -69,6 +77,7 @@ public PhysicalFileSystemCache(
6977
Directory.CreateDirectory(this.cacheRootPath);
7078
}
7179

80+
this.cachedNameLength = (int)this.options.CachedNameLength;
7281
this.fileProvider = new PhysicalFileProvider(this.cacheRootPath);
7382
this.options = options.Value;
7483
this.formatUtilies = formatUtilities;
@@ -77,7 +86,7 @@ public PhysicalFileSystemCache(
7786
/// <inheritdoc/>
7887
public async Task<IImageCacheResolver> GetAsync(string key)
7988
{
80-
string path = this.ToFilePath(key);
89+
string path = ToFilePath(key, this.cachedNameLength);
8190

8291
IFileInfo metaFileInfo = this.fileProvider.GetFileInfo(this.ToMetaDataFilePath(path));
8392
if (!metaFileInfo.Exists)
@@ -105,7 +114,7 @@ public async Task<IImageCacheResolver> GetAsync(string key)
105114
/// <inheritdoc/>
106115
public async Task SetAsync(string key, Stream stream, ImageCacheMetadata metadata)
107116
{
108-
string path = Path.Combine(this.cacheRootPath, this.ToFilePath(key));
117+
string path = Path.Combine(this.cacheRootPath, ToFilePath(key, this.cachedNameLength));
109118
string imagePath = this.ToImageFilePath(path, metadata);
110119
string metaPath = this.ToMetaDataFilePath(path);
111120
string directory = Path.GetDirectoryName(path);
@@ -146,8 +155,36 @@ private string ToImageFilePath(string path, in ImageCacheMetadata metaData)
146155
/// Converts the key into a nested file path.
147156
/// </summary>
148157
/// <param name="key">The cache key.</param>
158+
/// <param name="cachedNameLength">The length of the cached file name minus the extension.</param>
149159
/// <returns>The <see cref="string"/>.</returns>
150-
private string ToFilePath(string key) // TODO: Avoid the allocation here.
151-
=> $"{string.Join("/", key.Substring(0, (int)this.options.CachedNameLength).ToCharArray())}/{key}";
160+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
161+
internal static unsafe string ToFilePath(string key, int cachedNameLength)
162+
{
163+
const char separator = '/';
164+
165+
// Each key substring char + separator + key
166+
int length = (cachedNameLength * 2) + key.Length;
167+
fixed (char* keyPtr = key)
168+
{
169+
return string.Create(length, (Ptr: (IntPtr)keyPtr, key.Length), (chars, args) =>
170+
{
171+
var keySpan = new ReadOnlySpan<char>((char*)args.Ptr, args.Length);
172+
ref char keyRef = ref MemoryMarshal.GetReference(keySpan);
173+
ref char charRef = ref MemoryMarshal.GetReference(chars);
174+
175+
int index = 0;
176+
for (int i = 0; i < cachedNameLength; i++)
177+
{
178+
Unsafe.Add(ref charRef, index++) = Unsafe.Add(ref keyRef, i);
179+
Unsafe.Add(ref charRef, index++) = separator;
180+
}
181+
182+
for (int i = 0; i < keySpan.Length; i++)
183+
{
184+
Unsafe.Add(ref charRef, index++) = Unsafe.Add(ref keyRef, i);
185+
}
186+
});
187+
}
188+
}
152189
}
153190
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Runtime.CompilerServices;
6+
using System.Runtime.InteropServices;
7+
using System.Text;
8+
using BenchmarkDotNet.Attributes;
9+
10+
namespace SixLabors.ImageSharp.Web.Benchmarks.Caching
11+
{
12+
[Config(typeof(MemoryConfig))]
13+
public class StringJoinBenchmarks
14+
{
15+
private const string Key = "abcdefghijkl";
16+
private const int CachedNameLength = 12;
17+
18+
[Benchmark(Baseline = true, Description = "String.Join")]
19+
public string JoinUsingString()
20+
=> $"{string.Join("/", Key.Substring(0, CachedNameLength).ToCharArray())}/{Key}";
21+
22+
[Benchmark(Description = "StringBuilder.Append")]
23+
public string JoinUsingStringBuilder()
24+
{
25+
ReadOnlySpan<char> keySpan = Key;
26+
const char separator = '/';
27+
28+
// Each key substring char + separator + key
29+
var sb = new StringBuilder((CachedNameLength * 2) + Key.Length);
30+
ReadOnlySpan<char> paths = keySpan.Slice(0, CachedNameLength);
31+
for (int i = 0; i < paths.Length; i++)
32+
{
33+
sb.Append(paths[i]);
34+
sb.Append(separator);
35+
}
36+
37+
sb.Append(Key);
38+
return sb.ToString();
39+
}
40+
41+
[Benchmark(Description = "String.Create")]
42+
public unsafe string JoinUsingStringCreate()
43+
{
44+
ReadOnlySpan<char> keySpan = Key;
45+
const char separator = '/';
46+
47+
// Each key substring char + separator + key
48+
int length = (CachedNameLength * 2) + Key.Length;
49+
fixed (char* keyPtr = Key)
50+
{
51+
return string.Create(length, (Ptr: (IntPtr)keyPtr, Key.Length), (chars, args) =>
52+
{
53+
var keySpan = new ReadOnlySpan<char>((char*)args.Ptr, args.Length);
54+
ref char keyRef = ref MemoryMarshal.GetReference(keySpan);
55+
ref char charRef = ref MemoryMarshal.GetReference(chars);
56+
57+
int index = 0;
58+
for (int i = 0; i < CachedNameLength; i++)
59+
{
60+
Unsafe.Add(ref charRef, index++) = Unsafe.Add(ref keyRef, i);
61+
Unsafe.Add(ref charRef, index++) = separator;
62+
}
63+
64+
for (int i = 0; i < keySpan.Length; i++)
65+
{
66+
Unsafe.Add(ref charRef, index++) = Unsafe.Add(ref keyRef, i);
67+
}
68+
});
69+
}
70+
}
71+
}
72+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using SixLabors.ImageSharp.Web.Caching;
5+
using Xunit;
6+
7+
namespace SixLabors.ImageSharp.Web.Tests.Caching
8+
{
9+
public class PhysicialFileSystemCacheTests
10+
{
11+
[Fact]
12+
public void FilePathMatchesReference()
13+
{
14+
const string Key = "abcdefghijkl";
15+
const int CachedNameLength = 12;
16+
17+
string expected = $"{string.Join("/", Key.Substring(0, CachedNameLength).ToCharArray())}/{Key}";
18+
string actual = PhysicalFileSystemCache.ToFilePath(Key, CachedNameLength);
19+
20+
Assert.Equal(expected, actual);
21+
}
22+
}
23+
}

0 commit comments

Comments
 (0)