Introduce no GC string formatter to optimize score string formatting.

This commit is contained in:
2023-01-26 16:52:05 +08:00
parent c015b60dc3
commit 601f64cc61
9 changed files with 2863 additions and 13 deletions

View File

@@ -4,6 +4,7 @@ using Cryville.Common.Pdt;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Text.Formatting;
namespace Cryville.Crtr { namespace Cryville.Crtr {
public class Judge { public class Judge {
@@ -217,7 +218,7 @@ namespace Cryville.Crtr {
scoreDefs.Add(key, s.Value); scoreDefs.Add(key, s.Value);
scores.Add(key, s.Value.init); scores.Add(key, s.Value.init);
scoreStringCache.Add(scoreStringKeys[key], null); scoreStringCache.Add(scoreStringKeys[key], null);
scoreStringSrcs.Add(scoreStringKeys[key], new ScoreStringSrc(scoreStringPool, () => GetScoreString(strkey))); scoreStringSrcs.Add(scoreStringKeys[key], new ScoreStringSrc(scoreStringPool, () => scores[key], scoreDefs[key].format));
} }
} }
void InvalidateScore(int key) { void InvalidateScore(int key) {
@@ -249,30 +250,32 @@ namespace Cryville.Crtr {
return result; return result;
} }
class ScoreStringSrc : PropSrc { class ScoreStringSrc : PropSrc {
readonly Func<string> _cb; readonly Func<float> _cb;
readonly string _format;
readonly ArrayPool<byte> _pool; readonly ArrayPool<byte> _pool;
byte[] _buf; readonly StringBuffer _buf = new StringBuffer();
public ScoreStringSrc(ArrayPool<byte> pool, Func<string> cb) public ScoreStringSrc(ArrayPool<byte> pool, Func<float> cb, string format)
: base(PdtInternalType.String) { : base(PdtInternalType.String) {
_pool = pool; _pool = pool;
_cb = cb; _cb = cb;
_format = string.Format("{{0:{0}}}", format);
} }
public override void Invalidate() { public override void Invalidate() {
base.Invalidate(); if (buf != null) {
if (_buf != null) { _pool.Return(buf);
_pool.Return(_buf); base.Invalidate();
_buf = null;
} }
} }
protected override unsafe void InternalGet() { protected override unsafe void InternalGet() {
var src = _cb(); var src = _cb();
int strlen = src.Length; _buf.Clear();
_buf.AppendFormat(_format, src);
int strlen = _buf.Count;
buf = _pool.Rent(sizeof(int) + strlen * sizeof(char)); buf = _pool.Rent(sizeof(int) + strlen * sizeof(char));
fixed (byte* _ptr = buf) { fixed (byte* _ptr = buf) {
char* ptr = (char*)(_ptr + sizeof(int));
*(int*)_ptr = strlen; *(int*)_ptr = strlen;
int i = 0; char* ptr = (char*)(_ptr + sizeof(int));
foreach (var c in src) ptr[i++] = c; _buf.CopyTo(ptr, 0, strlen);
} }
} }
} }

View File

@@ -3,7 +3,8 @@
"rootNamespace": "", "rootNamespace": "",
"references": [ "references": [
"GUID:d8ea0e0da3ad53a45b65c912ffcacab0", "GUID:d8ea0e0da3ad53a45b65c912ffcacab0",
"GUID:5686e5ee69d0e084c843d61c240d7fdb" "GUID:5686e5ee69d0e084c843d61c240d7fdb",
"GUID:2922aa74af3b2854e81b8a8b286d8206"
], ],
"includePlatforms": [], "includePlatforms": [],
"excludePlatforms": [], "excludePlatforms": [],

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0c0e7d20046652343bdbe3ed52cb0340
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,333 @@
// Copyright (c) 2023 Cryville
//
// This file contains an implementation of the stub for custom format string
// originally written by Michael Popoloski. Below is the original license.
//
// Copyright (c) 2015-2017 Michael Popoloski
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
namespace System.Text.Formatting {
// this file contains the custom numeric formatting routines split out from the Numeric.cs file
unsafe partial class Numeric {
static void NumberToCustomFormatString(StringBuffer formatter, ref Number number, StringView specifier, CachedCulture culture) {
// Special: Handle special values
switch (number.Scale) {
case ScaleInf: formatter.Append(number.Sign == 0 ? culture.PositiveInfinity : culture.NegativeInfinity); return;
case ScaleNaN: formatter.Append(culture.NaN); return;
}
// Iteration 1: Split by semicolon
int specifierPositiveEnd = IndexOfSectionSeparator(specifier);
int specifierNegativeStart = 0, specifierNegativeEnd, specifierZeroStart = 0;
if (specifierPositiveEnd == -1) {
specifierPositiveEnd = specifierNegativeEnd = specifier.Length;
}
else {
specifierNegativeStart = specifierPositiveEnd + 1;
specifierNegativeEnd = IndexOfSectionSeparator(specifier, specifierNegativeStart);
if (specifierNegativeEnd == -1) {
specifierNegativeEnd = specifier.Length;
}
else {
specifierZeroStart = specifierNegativeEnd + 1;
}
}
// Special: Handle zero
if (IsZero(ref number)) {
FormatCustomFormatString(formatter, ref number, null, specifier, specifierZeroStart, specifier.Length, culture);
return;
}
// Iteration 2: Divide and round number
int originalScale = number.Scale;
if (number.Sign == 0) ApplyDivisionAndPrecision(ref number, specifier, 0, specifierPositiveEnd);
else ApplyDivisionAndPrecision(ref number, specifier, specifierNegativeStart, specifierNegativeEnd);
// Iteration 3: Count; Iteration 4: Format
if (IsZero(ref number)) FormatCustomFormatString(formatter, ref number, null, specifier, specifierZeroStart, specifier.Length, culture);
else if (number.Sign == 0) FormatCustomFormatString(formatter, ref number, originalScale, specifier, 0, specifierPositiveEnd, culture);
else {
if (specifierNegativeStart == 0) formatter.Append(culture.NegativeSign);
FormatCustomFormatString(formatter, ref number, originalScale, specifier, specifierNegativeStart, specifierNegativeEnd, culture);
}
}
static int IndexOfSectionSeparator(StringView specifier) {
return IndexOfSectionSeparator(specifier, 0);
}
static int IndexOfSectionSeparator(StringView specifier, int index) {
if (index < 0 || index > specifier.Length) throw new ArgumentOutOfRangeException("index");
char* ptr = specifier.Data;
for (; index < specifier.Length; index++) {
switch (ptr[index]) {
case ';': return index;
case '\\':
index++;
if (index >= specifier.Length) throw new FormatException();
break;
case '\'':
SkipLiteral(specifier, ref index, '\'');
break;
case '"':
SkipLiteral(specifier, ref index, '"');
break;
}
}
return -1;
}
static bool IsZero(ref Number number) {
char* ptr = number.Digits;
while (*ptr != '\0') if (*ptr++ != '0') return false;
return true;
}
static void ApplyDivisionAndPrecision(ref Number number, StringView specifier, int index, int end) {
int deltaScale = 0, scalingSpecifiers = 0;
int integralDigits = 0, decimalDigits = 0;
bool decimalFlag = false, exponentialFlag = false;
char* ptr = specifier.Data;
for (; index < end; index++) {
switch (ptr[index]) {
case '\\':
if (++index >= end) throw new FormatException();
break;
case '\'':
SkipLiteral(specifier, ref index, '\'');
break;
case '"':
SkipLiteral(specifier, ref index, '"');
break;
case '0':
case '#':
if (decimalFlag) decimalDigits++;
else integralDigits++;
scalingSpecifiers = 0;
break;
case '.': decimalFlag = true; deltaScale -= scalingSpecifiers * 3; break;
case ',': scalingSpecifiers++; break;
case '%': deltaScale += 2; break;
case '‰': deltaScale += 3; break;
case 'E':
case 'e':
if (++index >= end) goto exit;
char tc0 = ptr[index];
if (tc0 == '+' || tc0 == '-') {
if (++index >= end) goto exit;
}
if (ptr[index] != '0') break;
exponentialFlag = true;
for (index++; index < end && ptr[index] == '0'; index++) ;
index--;
break;
}
}
exit:
if (exponentialFlag) {
number.Scale = integralDigits;
RoundNumber(ref number, integralDigits + decimalDigits);
}
else {
number.Scale += deltaScale;
RoundNumber(ref number, number.Scale + decimalDigits);
}
}
static void FormatCustomFormatString(StringBuffer formatter, ref Number number, int? originalScale, StringView specifier, int index, int end, CachedCulture culture) {
int start = index;
int integralDigits = 0, integralZeros = 0,
decimalDigits = 0, decimalZeros = 0;
bool integralHasZeroFlag = false;
int exponentialZeros = 0;
bool decimalFlag = false;
bool commaFlag = false, groupFlag = false;
char* ptr = specifier.Data;
for (; index < end; index++) {
switch (ptr[index]) {
case '\\':
if (++index >= end) throw new FormatException();
break;
case '\'':
SkipLiteral(specifier, ref index, '\'');
break;
case '"':
SkipLiteral(specifier, ref index, '"');
break;
case '0':
if (decimalFlag) {
if (decimalZeros < decimalDigits)
decimalZeros = decimalDigits;
decimalDigits++;
decimalZeros++;
}
else {
integralDigits++;
integralZeros++;
integralHasZeroFlag = true;
if (commaFlag) groupFlag = true;
}
break;
case '#':
if (decimalFlag) {
decimalDigits++;
}
else {
integralDigits++;
if (integralHasZeroFlag) integralZeros++;
if (commaFlag) groupFlag = true;
}
break;
case '.': decimalFlag = true; commaFlag = false; break;
case ',': commaFlag = true; break;
case 'E':
case 'e':
if (++index >= end) goto exit;
char tc0 = ptr[index];
if (tc0 == '+' || tc0 == '-') {
if (++index >= end) goto exit;
}
if (ptr[index] != '0') break;
for (; index < end && ptr[index] == '0'; index++) exponentialZeros++;
index--;
break;
}
}
exit:
int currentDigitIndex = 0;
int groupIndex = 0, remainingDigitsInGroup = Math.Max(number.Scale, decimalZeros);
if (groupFlag) while (true) {
int groupSize = culture.NumberData.GroupSizes.ElementAtOrLast(groupIndex);
if (remainingDigitsInGroup <= groupSize) break;
remainingDigitsInGroup -= groupSize;
groupIndex++;
}
if (number.Scale > integralDigits) while (currentDigitIndex < number.Scale - integralDigits)
formatter.AppendIntegralDigit(ref number, ref currentDigitIndex, culture, groupFlag, ref groupIndex, ref remainingDigitsInGroup);
decimalFlag = false;
for (index = start; index < end; index++) {
switch (ptr[index]) {
case '\\':
if (++index >= end) throw new FormatException();
formatter.Append(ptr[index]);
break;
case '\'':
formatter.AppendLiteral(specifier, ref index, '\'');
break;
case '"':
formatter.AppendLiteral(specifier, ref index, '"');
break;
case '0':
case '#':
if (decimalFlag) {
if (currentDigitIndex < number.Precision) {
char digit = number.Digits[currentDigitIndex++];
if (digit == '\0') digit = '0';
formatter.Append(digit);
}
else if (decimalZeros > 0)
formatter.Append('0');
--decimalDigits;
--decimalZeros;
}
else {
if (integralDigits <= number.Scale)
formatter.AppendIntegralDigit(ref number, ref currentDigitIndex, culture, groupFlag, ref groupIndex, ref remainingDigitsInGroup);
else if (integralDigits <= integralZeros)
formatter.AppendIntegralDigit('0', culture, groupFlag, ref groupIndex, ref remainingDigitsInGroup);
--integralDigits;
}
break;
case '.': formatter.Append(culture.NumberData.DecimalSeparator); decimalFlag = true; break;
case ',': break;
case '%': formatter.Append(culture.PercentSymbol); break;
case '‰': formatter.Append(culture.PerMilleSymbol); break;
case 'E':
case 'e':
char exponentialSymbol = ptr[index];
if (++index >= end) {
index--;
goto default;
}
char tc0 = ptr[index];
bool hasPlusFlag = false;
if (tc0 == '+' || tc0 == '-') {
if (++index >= end) {
index -= 2;
goto default;
}
if (tc0 == '+') hasPlusFlag = true;
}
if (ptr[index] != '0') {
index -= 2;
goto default;
}
for (index++; index < end && ptr[index] == '0'; index++) ;
index--;
int exp = originalScale == null ? 0 : originalScale.Value - number.Scale;
formatter.Append(exponentialSymbol);
if (exp < 0) {
formatter.Append(culture.NegativeSign);
exp = -exp;
}
else if (hasPlusFlag) {
formatter.Append(culture.PositiveSign);
}
Int32ToDecStr(formatter, exp, exponentialZeros, "");
break;
default:
formatter.Append(ptr[index]);
break;
}
}
}
static void AppendIntegralDigit(this StringBuffer self, ref Number number, ref int index, CachedCulture culture, bool groupFlag, ref int groupIndex, ref int remainingDigitsInGroup) {
char digit;
if (index >= number.Precision) digit = '0';
else digit = number.Digits[index];
if (digit == '\0') digit = '0';
self.AppendIntegralDigit(digit, culture, groupFlag, ref groupIndex, ref remainingDigitsInGroup);
index++;
}
static void AppendIntegralDigit(this StringBuffer self, char digit, CachedCulture culture, bool groupFlag, ref int groupIndex, ref int remainingDigitsInGroup) {
self.Append(digit);
if (!groupFlag) return;
if (--remainingDigitsInGroup == 0) {
if (--groupIndex >= 0) {
remainingDigitsInGroup = culture.NumberData.GroupSizes.ElementAtOrLast(groupIndex);
self.Append(culture.NumberData.GroupSeparator);
}
}
}
static T ElementAtOrLast<T>(this T[] arr, int index) {
if (index >= arr.Length) index = arr.Length - 1;
return arr[index];
}
static void SkipLiteral(StringView specifier, ref int index, char delimiter) {
while (++index < specifier.Length) {
if (specifier.Data[index] == delimiter) return;
}
throw new FormatException();
}
static void AppendLiteral(this StringBuffer self, StringView specifier, ref int index, char delimiter) {
int start = index + 1;
while (++index < specifier.Length) {
if (specifier.Data[index] == delimiter) {
self.Append(specifier.Data + start, index - start);
return;
}
}
throw new FormatException();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 85c9d5fa7edeb9d4d8737903efc95abd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,14 @@
{
"name": "StringFormatter",
"rootNamespace": "",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": true
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 2922aa74af3b2854e81b8a8b286d8206
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: be273397b1d98a24cb864cb3e05643cd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: