Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System;
using BenchmarkDotNet.Attributes;

namespace SkiaSharp.Benchmarks;

// New vs Old for SKFourByteTag.Parse(string).
//
// Old = the current shipped implementation (allocates a char[4] scratch buffer per call).
// New = the proposed managed fast path (no allocation; reads chars directly).
//
// The real caller shape is font/OpenType tag hydration: feature tags ("liga", "kern"),
// variation-axis tags ("wght", "wdth", "slnt"), and table tags parsed from strings, often a
// batch of many at once when configuring a font. This benchmark parses a representative batch
// of such tags (plus null/empty/short/over-long edges) so both the happy path and the padding
// branches are exercised.
[MemoryDiagnoser]
public class SKFourByteTagParseBenchmark
{
private string[] tags;

[GlobalSetup]
public void GlobalSetup()
{
// A realistic mix of 4-char tags, plus edge shapes that hit the padding/truncation code.
tags = new[]
{
"liga", "kern", "wght", "wdth", "slnt", "ital", "opsz", "GSUB",
"GPOS", "cmap", "head", "name", "OS/2", "post", "glyf", "loca",
"a", "bc", "def", "toolong", "", null,
};
}

[Benchmark(Baseline = true)]
public uint Old()
{
uint sink = 0;
foreach (var t in tags)
sink ^= OldParse(t);
return sink;
}

[Benchmark]
public uint New()
{
uint sink = 0;
foreach (var t in tags)
sink ^= (uint)SKFourByteTag.Parse(t);
return sink;
}

// Verbatim copy of the ORIGINAL shipped Parse(string) body, so the ratio is honest.
private static uint OldParse(string tag)
{
if (string.IsNullOrEmpty(tag))
return 0;

var realTag = new char[4];
var len = Math.Min(4, tag.Length);
var i = 0;
for (; i < len; i++)
realTag[i] = tag[i];
for (; i < 4; i++)
realTag[i] = ' ';

return (uint)(((byte)realTag[0] << 24) | ((byte)realTag[1] << 16) | ((byte)realTag[2] << 8) | (byte)realTag[3]);
}
}
24 changes: 14 additions & 10 deletions binding/SkiaSharp/SKFourByteTag.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,24 @@ public SKFourByteTag (char c1, char c2, char c3, char c4)
value = (uint)(((byte)c1 << 24) | ((byte)c2 << 16) | ((byte)c3 << 8) | (byte)c4);
}

public static SKFourByteTag Parse (string? tag)
public static SKFourByteTag Parse (string? tag) =>
Parse (tag.AsSpan ());

public static SKFourByteTag Parse (ReadOnlySpan<char> tag)
{
if (string.IsNullOrEmpty (tag))
if (tag.IsEmpty)
return new SKFourByteTag (0);

var realTag = new char[4];
var len = Math.Min (4, tag!.Length);
var i = 0;
for (; i < len; i++)
realTag[i] = tag[i];
for (; i < 4; i++)
realTag[i] = ' ';
// Take up to the first four characters, padding any missing trailing
// slots with spaces — matching the original char[4]-scratch behaviour
// without allocating the scratch array. The first character always
// exists here because empty input was handled by the guard above.
var c1 = tag[0];
var c2 = tag.Length > 1 ? tag[1] : ' ';
var c3 = tag.Length > 2 ? tag[2] : ' ';
var c4 = tag.Length > 3 ? tag[3] : ' ';

return new SKFourByteTag (realTag[0], realTag[1], realTag[2], realTag[3]);
return new SKFourByteTag (c1, c2, c3, c4);
}

public override string ToString () =>
Expand Down
56 changes: 56 additions & 0 deletions tests/Tests/SkiaSharp/SKFourByteTagTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,62 @@ public void FourByteTagEqualsObjectWorks ()
Assert.False (tag.Equals ("wght")); // wrong type
}

// Expected values are precomputed constants rather than the output of a runtime reference
// implementation, so the oracle can never silently drift along with the code under test.
// Each expected value is the big-endian packing of the low byte of the first four UTF-16
// code units, padding any missing trailing slots with spaces (0x20) and treating
// null/empty as zero.
[Theory]
[InlineData (null, 0x00000000u)]
[InlineData ("", 0x00000000u)]
[InlineData (" ", 0x20202020u)]
[InlineData ("a", 0x61202020u)]
[InlineData ("ab", 0x61622020u)]
[InlineData ("abc", 0x61626320u)]
[InlineData ("abcd", 0x61626364u)]
[InlineData ("abcde", 0x61626364u)]
[InlineData ("abcdef", 0x61626364u)]
[InlineData ("wght", 0x77676874u)]
[InlineData ("wdth", 0x77647468u)]
[InlineData ("slnt", 0x736C6E74u)]
[InlineData ("opsz", 0x6F70737Au)]
[InlineData ("ital", 0x6974616Cu)]
[InlineData ("GSUB", 0x47535542u)]
[InlineData ("GPOS", 0x47504F53u)]
[InlineData ("OS/2", 0x4F532F32u)]
[InlineData (" ", 0x20202020u)]
[InlineData ("\0\0\0\0", 0x00000000u)]
[InlineData ("\t\r\n ", 0x090D0A20u)]
// characters above 0xFF: only the low byte is kept
[InlineData ("\u0100\u0141\u2764\uFFFF", 0x004164FFu)]
[InlineData ("\u00e9\u00e9\u00ffz", 0xE9E9FF7Au)]
[InlineData ("\uABCDwgh", 0xCD776768u)]
// surrogate code units are still just char code units to the algorithm
[InlineData ("\uD83D\uDE00xy", 0x3D007879u)]
public void FourByteTagParseProducesExpectedValue (string input, uint expected)
{
// Both the string overload and the span overload must produce the same constant.
Assert.Equal (expected, (uint)SKFourByteTag.Parse (input));
Assert.Equal (expected, (uint)SKFourByteTag.Parse (input.AsSpan ()));
}

[Fact]
public void FourByteTagParseSpanFromSlicedStringMatches ()
{
// A span carved out of a larger string (no substring allocation) must parse the same
// as the equivalent standalone string.
const string source = "xxwghtyy";
var slice = source.AsSpan (2, 4);
Assert.Equal ((uint)SKFourByteTag.Parse ("wght"), (uint)SKFourByteTag.Parse (slice));
}

[Fact]
public void FourByteTagParseEmptySpanReturnsZero ()
{
Assert.Equal (0u, (uint)SKFourByteTag.Parse (ReadOnlySpan<char>.Empty));
Assert.Equal (0u, (uint)SKFourByteTag.Parse (default (ReadOnlySpan<char>)));
}

[Fact]
public void FourByteTagSurvivesNativeRoundTrip ()
{
Expand Down
Loading