编辑编辑:由 The General 和 Mirai Mann 更新为最新版本:
如果您想知道哪匹马跑得最快:赛马。这里是BenchmarkDotNet 比较这个问题的各种答案的结果(我没有将他们的代码合并到我的完整示例中,因为感觉不对 - 只显示数字)具有可重复但大量随机输入,通过:
static MyTests()
{
Random rand = new Random(12345);
StringBuilder input = new StringBuilder();
string operators = "+-*/";
var lastOperator = '+';
for (int i = 0; i < 1000000; i++)
{
var @operator = operators[rand.Next(0, 4)];
input.Append(rand.Next(lastOperator == '/' ? 1 : 0, 100) + " " + @operator + " ");
lastOperator = @operator;
}
input.Append(rand.Next(0, 100));
expression = input.ToString();
}
private static readonly string expression;
进行健全性检查(检查他们都做正确的事):
Original: -1426
NoSubStrings: -1426
NoSubStringsUnsafe: -1426
TheGeneral4: -1426
MiraiMann1: -1426
我们得到了时间安排(注意:Original 是问题中的 OP 版本;NoSubStrings[Unsafe] 是我下面的版本,以及其他两个版本的用户名):
(“平均值”越低越好)
(注意;Mirai Mann 的实现有一个更新的版本,但我不再有运行新测试的设置;但是:公平地假设它应该会更好!)
运行时:.NET Framework 4.7 (CLR 4.0.30319.42000),32 位 LegacyJIT-v4.7.2633.0
Method | Mean | Error | StdDev |
------------------- |----------:|----------:|----------:|
Original | 104.11 ms | 1.4920 ms | 1.3226 ms |
NoSubStrings | 21.99 ms | 0.4335 ms | 0.7122 ms |
NoSubStringsUnsafe | 20.53 ms | 0.4103 ms | 0.6967 ms |
TheGeneral4 | 15.50 ms | 0.3020 ms | 0.5369 ms |
MiraiMann1 | 15.54 ms | 0.3096 ms | 0.4133 ms |
运行时:.NET Framework 4.7 (CLR 4.0.30319.42000),64 位 RyuJIT-v4.7.2633.0
Method | Mean | Error | StdDev | Median |
------------------- |----------:|----------:|----------:|----------:|
Original | 114.15 ms | 1.3142 ms | 1.0974 ms | 114.13 ms |
NoSubStrings | 21.33 ms | 0.4161 ms | 0.6354 ms | 20.93 ms |
NoSubStringsUnsafe | 19.24 ms | 0.3832 ms | 0.5245 ms | 19.43 ms |
TheGeneral4 | 13.97 ms | 0.2795 ms | 0.2745 ms | 13.86 ms |
MiraiMann1 | 15.60 ms | 0.3090 ms | 0.4125 ms | 15.53 ms |
运行时:.NET Core 2.1.0-preview1-26116-04(CoreCLR 4.6.26116.03,CoreFX 4.6.26116.01),64 位 RyuJIT
Method | Mean | Error | StdDev | Median |
------------------- |----------:|----------:|----------:|----------:|
Original | 101.51 ms | 1.7807 ms | 1.5786 ms | 101.44 ms |
NoSubStrings | 21.36 ms | 0.4281 ms | 0.5414 ms | 21.07 ms |
NoSubStringsUnsafe | 19.85 ms | 0.4172 ms | 0.6737 ms | 19.80 ms |
TheGeneral4 | 14.06 ms | 0.2788 ms | 0.3723 ms | 13.82 ms |
MiraiMann1 | 15.88 ms | 0.3153 ms | 0.5922 ms | 15.45 ms |
我添加BenchmarkDotNet之前的原始答案:
如果我尝试这样做,我会很想看看 2.1 预览版中的 Span<T> 工作 - 关键是 Span<T> 可以在不分配的情况下进行切片(并且string 可以转换为 Span<char> 而无需分配);这将允许在没有任何分配的情况下执行字符串雕刻和解析。但是,减少分配并不总是与原始性能完全相同(尽管它们是相关的),因此要知道它是否更快:您需要与马赛跑(即比较它们)。
如果Span<T> 不是一个选项:您仍然可以通过手动跟踪int offset 来做同样的事情,只是*从不使用SubString)
在任何一种情况下(string 或 Span<char>):如果您的操作只需要处理整数表示的某个子集,我可能会想给角色一个不适用的自定义 int.Parse 等效项尽可能多的规则(文化、替代布局等),并且无需 Substring 即可工作 - 例如,它可能需要 string 和 ref int offset,其中 offset 更新为 解析停止(因为它碰到了一个运算符或一个空格),并且它起作用了。
类似:
static class P
{
static void Main()
{
string input = "14 + 2 * 32 / 60 + 43 - 7 + 3 - 1 + 0 * 7 + 87 - 32 / 34";
var val = Evaluate(input);
System.Console.WriteLine(val);
}
static int Evaluate(string expression)
{
int offset = 0;
SkipSpaces(expression, ref offset);
int value = ReadInt32(expression, ref offset);
while(ReadNext(expression, ref offset, out char @operator, out int operand))
{
switch(@operator)
{
case '+': value = value + operand; break;
case '-': value = value - operand; break;
case '*': value = value * operand; break;
case '/': value = value / operand; break;
}
}
return value;
}
static bool ReadNext(string value, ref int offset,
out char @operator, out int operand)
{
SkipSpaces(value, ref offset);
if(offset >= value.Length)
{
@operator = (char)0;
operand = 0;
return false;
}
@operator = value[offset++];
SkipSpaces(value, ref offset);
if (offset >= value.Length)
{
operand = 0;
return false;
}
operand = ReadInt32(value, ref offset);
return true;
}
static void SkipSpaces(string value, ref int offset)
{
while (offset < value.Length && value[offset] == ' ') offset++;
}
static int ReadInt32(string value, ref int offset)
{
bool isNeg = false;
char c = value[offset++];
int i = (c - '0');
if(c == '-')
{
isNeg = true;
i = 0;
// todo: what to do here if `-` is not followed by [0-9]?
}
while (offset < value.Length && (c = value[offset++]) >= '0' && c <= '9')
i = (i * 10) + (c - '0');
return isNeg ? -i : i;
}
}
接下来,我可能会考虑是否值得切换到unsafe 以删除双长度检查。所以我会以两种方式实现它,然后用 BenchmarkDotNet 之类的东西对其进行测试,看看它是否值得。
编辑:这是添加了unsafe 和 BenchmarkDotNet 用法:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
static class P
{
static void Main()
{
var summary = BenchmarkRunner.Run<MyTests>();
System.Console.WriteLine(summary);
}
}
public class MyTests
{
const string expression = "14 + 2 * 32 / 60 + 43 - 7 + 3 - 1 + 0 * 7 + 87 - 32 / 34";
[Benchmark]
public int Original() => EvalOriginal.Calc(expression);
[Benchmark]
public int NoSubStrings() => EvalNoSubStrings.Evaluate(expression);
[Benchmark]
public int NoSubStringsUnsafe() => EvalNoSubStringsUnsafe.Evaluate(expression);
}
static class EvalOriginal
{
public static int Calc(string sInput)
{
int iCurrent = sInput.IndexOf(' ');
int iResult = int.Parse(sInput.Substring(0, iCurrent));
int iNext = 0;
while ((iNext = sInput.IndexOf(' ', iCurrent + 4)) != -1)
{
iResult = Operate(iResult, sInput[iCurrent + 1], int.Parse(sInput.Substring((iCurrent + 3), iNext - (iCurrent + 2))));
iCurrent = iNext;
}
return Operate(iResult, sInput[iCurrent + 1], int.Parse(sInput.Substring((iCurrent + 3))));
}
public static int Operate(int iReturn, char cOperator, int iOperant)
{
switch (cOperator)
{
case '+':
return (iReturn + iOperant);
case '-':
return (iReturn - iOperant);
case '*':
return (iReturn * iOperant);
case '/':
return (iReturn / iOperant);
default:
throw new Exception("Error");
}
}
}
static class EvalNoSubStrings {
public static int Evaluate(string expression)
{
int offset = 0;
SkipSpaces(expression, ref offset);
int value = ReadInt32(expression, ref offset);
while (ReadNext(expression, ref offset, out char @operator, out int operand))
{
switch (@operator)
{
case '+': value = value + operand; break;
case '-': value = value - operand; break;
case '*': value = value * operand; break;
case '/': value = value / operand; break;
default: throw new Exception("Error");
}
}
return value;
}
static bool ReadNext(string value, ref int offset,
out char @operator, out int operand)
{
SkipSpaces(value, ref offset);
if (offset >= value.Length)
{
@operator = (char)0;
operand = 0;
return false;
}
@operator = value[offset++];
SkipSpaces(value, ref offset);
if (offset >= value.Length)
{
operand = 0;
return false;
}
operand = ReadInt32(value, ref offset);
return true;
}
static void SkipSpaces(string value, ref int offset)
{
while (offset < value.Length && value[offset] == ' ') offset++;
}
static int ReadInt32(string value, ref int offset)
{
bool isNeg = false;
char c = value[offset++];
int i = (c - '0');
if (c == '-')
{
isNeg = true;
i = 0;
}
while (offset < value.Length && (c = value[offset++]) >= '0' && c <= '9')
i = (i * 10) + (c - '0');
return isNeg ? -i : i;
}
}
static unsafe class EvalNoSubStringsUnsafe
{
public static int Evaluate(string expression)
{
fixed (char* ptr = expression)
{
int len = expression.Length;
var c = ptr;
SkipSpaces(ref c, ref len);
int value = ReadInt32(ref c, ref len);
while (len > 0 && ReadNext(ref c, ref len, out char @operator, out int operand))
{
switch (@operator)
{
case '+': value = value + operand; break;
case '-': value = value - operand; break;
case '*': value = value * operand; break;
case '/': value = value / operand; break;
default: throw new Exception("Error");
}
}
return value;
}
}
static bool ReadNext(ref char* c, ref int len,
out char @operator, out int operand)
{
SkipSpaces(ref c, ref len);
if (len-- == 0)
{
@operator = (char)0;
operand = 0;
return false;
}
@operator = *c++;
SkipSpaces(ref c, ref len);
if (len == 0)
{
operand = 0;
return false;
}
operand = ReadInt32(ref c, ref len);
return true;
}
static void SkipSpaces(ref char* c, ref int len)
{
while (len != 0 && *c == ' ') { c++;len--; }
}
static int ReadInt32(ref char* c, ref int len)
{
bool isNeg = false;
char ch = *c++;
len--;
int i = (ch - '0');
if (ch == '-')
{
isNeg = true;
i = 0;
}
while (len-- != 0 && (ch = *c++) >= '0' && ch <= '9')
i = (i * 10) + (ch - '0');
return isNeg ? -i : i;
}
}