【问题标题】:Fastest way to solve chain-calculations解决链式计算的最快方法
【发布时间】:2018-03-22 08:46:52
【问题描述】:

我有一个类似的输入

string input = "14 + 2 * 32 / 60 + 43 - 7 + 3 - 1 + 0 * 7 + 87 - 32 / 34"; 
// up to 10MB string size

int result = Calc(input); // 11
  • 计算是从左到右,逐个数字
  • 数字是 0 到 99
  • 加法前的乘法被忽略,所以14 + 2 * 32512
  • 可能的计算是+-*/
  • 除以0 是不可能的,所以在/ 之后不能是0

我的方法

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");
    }
}

我需要最快的方法来获得结果。

问题:有没有办法让这个计算更快?我有多个线程,但我只使用一个。

更新:

测试用例:(我已经删除了除以 0 的错误,并从 StopWatch 测量中删除了 StringBuilder.ToString()

Random rand = new Random();
System.Text.StringBuilder input = new System.Text.StringBuilder();
string operators = "+-*/";
input.Append(rand.Next(0, 100));
for (int i = 0; i < 1000000; i++)
{
    int number = rand.Next(0, 100);
    char coperator = operators[rand.Next(0, number > 0 ? 4 : 3)];
    input.Append(" " + coperator + " " + number);
}
string calc = input.ToString();
System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
watch.Start();
int result = Calc(calc);
watch.Stop();

【问题讨论】:

  • @john 不,这里的正确答案是 0。
  • @john OP 声明 “行前点被忽略”
  • "*", "/" = "Point" , "+", "-" = "Line" 源自“非数字”书写中的运算符。 (点和:用于乘法/除法)
  • @john 它可能有一个更正式的名称,但在德国小学中通常被称为“Punkt vor Strich”。
  • 很酷,您发布了一个带有时间测量的测试用例(+1)。不幸的是,它创建了您可以通过0 划分的示例。

标签: c# performance


【解决方案1】:

编辑编辑:由 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&lt;T&gt; 工作 - 关键是 Span&lt;T&gt; 可以在不分配的情况下进行切片(并且string 可以转换为 Span&lt;char&gt; 而无需分配);这将允许在没有任何分配的情况下执行字符串雕刻和解析。但是,减少分配并不总是与原始性能完全相同(尽管它们是相关的),因此要知道它是否更快:您需要与马赛跑(即比较它们)。

如果Span&lt;T&gt; 不是一个选项:您仍然可以通过手动跟踪int offset 来做同样的事情,只是*从不使用SubString

在任何一种情况下(stringSpan&lt;char&gt;):如果您的操作只需要处理整数表示的某个子集,我可能会想给角色一个不适用的自定义 int.Parse 等效项尽可能多的规则(文化、替代布局等),并且无需 Substring 即可工作 - 例如,它可能需要 stringref 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;
    }
}

【讨论】:

  • 感谢您的 Linq 评论。我想我很想走“方便”的方式。 :) 感谢您的课程。
  • 发出指令不是更快吗?那么所有的运行时/处理器优化都可以发生吗?还是我错了?
  • @kuskmen 不会,除非表达式会一遍又一遍地相同; emit 实际上是 slooooow - 只有当您可以缓存发出的 IL 并为不同的输入多次重复使用它时,它才会变得更快,这里不适用 .将每个表达式生成的 IL 存储在缓存中是没有意义的——当你这样做的时候,你不妨存储每个表达式的 result
  • 哈哈太棒了,我的绝地反优化技能令人恐惧!
  • @TheGeneral 我一直都在反优化(偶然);重要的是:拥有像 BenchmarkDotNet 这样的工具,以便我们注意到 :)
【解决方案2】:

下面的解是一个有限自动机。计算(输入)= O(n)。为了获得更好的性能,此解决方案不使用IndexOfSubstringParse、字符串连接或重复读取值(多次获取input[i])......只是一个字符处理器。

    static int Calculate1(string input)
    {
        int acc = 0;
        char last = ' ', operation = '+';

        for (int i = 0; i < input.Length; i++)
        {
            var current = input[i];
            switch (current)
            {
                case ' ':
                    if (last != ' ')
                    {
                        switch (operation)
                        {
                            case '+': acc += last - '0'; break;
                            case '-': acc -= last - '0'; break;
                            case '*': acc *= last - '0'; break;
                            case '/': acc /= last - '0'; break;
                        }

                        last = ' ';
                    }

                    break;

                case '0': case '1': case '2': case '3': case '4':
                case '5': case '6': case '7': case '8': case '9':
                    if (last == ' ') last = current;
                    else
                    {
                        var num = (last - '0') * 10 + (current - '0');
                        switch (operation)
                        {
                            case '+': acc += num; break;
                            case '-': acc -= num; break;
                            case '*': acc *= num; break;
                            case '/': acc /= num; break;
                        }
                        last = ' ';
                    }
                    break;

                case '+': case '-': case '*': case '/':
                    operation = current;
                    break;
            }
        }

        if (last != ' ')
            switch (operation)
            {
                case '+': acc += last - '0'; break;
                case '-': acc -= last - '0'; break;
                case '*': acc *= last - '0'; break;
                case '/': acc /= last - '0'; break;
            }

        return acc;
    }

还有另一个实现。它从输入中读取的内容减少了 25%。我预计它的性能会提高 25%。

    static int Calculate2(string input)
    {
        int acc = 0, i = 0;
        char last = ' ', operation = '+';

        while (i < input.Length)
        {
            var current = input[i];
            switch (current)
            {
                case ' ':
                    if (last != ' ')
                    {
                        switch (operation)
                        {
                            case '+': acc += last - '0'; break;
                            case '-': acc -= last - '0'; break;
                            case '*': acc *= last - '0'; break;
                            case '/': acc /= last - '0'; break;
                        }

                        last = ' ';
                        i++;
                    }

                    break;

                case '0': case '1': case '2': case '3': case '4':
                case '5': case '6': case '7': case '8': case '9':
                    if (last == ' ')
                    {
                        last = current;
                        i++;
                    }
                    else
                    {
                        var num = (last - '0') * 10 + (current - '0');
                        switch (operation)
                        {
                            case '+': acc += num; break;
                            case '-': acc -= num; break;
                            case '*': acc *= num; break;
                            case '/': acc /= num; break;
                        }

                        last = ' ';
                        i += 2;
                    }
                    break;

                case '+': case '-': case '*': case '/':
                    operation = current;
                    i += 2;
                    break;
            }
        }

        if (last != ' ')
            switch (operation)
            {
                case '+': acc += last - '0'; break;
                case '-': acc -= last - '0'; break;
                case '*': acc *= last - '0'; break;
                case '/': acc /= last - '0'; break;
            }

        return acc;
    }

我又实现了一个变体:

    static int Calculate3(string input)
    {
        int acc = 0, i = 0;
        var operation = '+';

        while (true)
        {
            var a = input[i++] - '0';
            if (i == input.Length)
            {
                switch (operation)
                {
                    case '+': acc += a; break;
                    case '-': acc -= a; break;
                    case '*': acc *= a; break;
                    case '/': acc /= a; break;
                }

                break;
            }

            var b = input[i];
            if (b == ' ') i++;
            else
            {
                a = a * 10 + (b - '0');
                i += 2;
            }

            switch (operation)
            {
                case '+': acc += a; break;
                case '-': acc -= a; break;
                case '*': acc *= a; break;
                case '/': acc /= a; break;
            }

            if (i >= input.Length) break;
            operation = input[i];
            i += 2;
        }

        return acc;
    }

产生抽象点:

  • 计算1 230
  • 计算2 192
  • 计算3 111

【讨论】:

  • 我会使用foreach 而不是for (当字符串已经在内存中时),但我不确定它是否更快。但总的来说,我更喜欢使用 StreamReader 并读取字符 (当输入是文件或其他流时) 来减少大“字符串”的内存负载。否则我对这个问题的解决方案可能是相似的。
  • 'for' 比 'foreach' 快。我们可以按缓冲区加载缓冲区并处理每个缓冲区。在这种情况下,当前算法也是实际的。
  • @Dr.Snail 对),当案例以个位数结束时,我的两种算法都出错了。固定。
  • “'for' 比 'foreach' 快”——仍然是这样吗?从 C# 编译器(或 JIT,不关心)来看,这将是非常糟糕的。优化是微不足道的。数组/字符串上的foreach 至少应该和for 一样快,没有理由不这样做。
  • @kuskmen 你必须测试一下
【解决方案3】:

注意

对于每个 cmets,此答案并未提供高效的解决方案。 我将把它留在这里,因为有一些要点需要考虑/其他人将来可能会发现这个线程;但正如人们在下面所说的那样,这远非最高效的解决方案。


原答案

.net 框架已经提供了一种方法来处理以字符串形式给出的公式:

var formula = "14 + 2 * 32 / 60 + 43 - 7 + 3 - 1 + 0 * 7 + 87 - 32 / 34";
var result = new DataTable().Compute(formula, null);
Console.WriteLine(result); //returns 139.125490196078

基于 cmets 的初步反馈

根据 cmets 线程,我需要指出一些事情:

这是否按照我描述的方式工作?

没有;这符合数学的正常规则。

我假设您修改的规则是为了简化编写代码来处理它们,而不是因为您想支持一个新的数学分支?如果是这样的话,我会反对。人们会期望事物以某种方式表现;因此,您必须确保任何将方程式发送到您的代码的人都已准备好期望这种新数学规则的知识,而不是能够使用他们现有的期望。

这里没有更改规则的选项;所以如果你的要求是改变数学规则,这对你不起作用。

这是最快的解决方案吗

没有。然而,考虑到 MS 花了很多时间优化他们的代码,它应该表现得很好,因此可能会比任何手动代码更快地执行相同的操作(尽管不可否认,这段代码不仅仅支持四个主要运算符;所以不完全一样)。

根据 MatthewWatson 的具体评论(即调用 DataTable 构造函数会产生大量开销),您希望创建并重用该对象的一个​​实例。根据您的解决方案的外观,有多种方法可以做到这一点;这是一个:

interface ICalculator //if we use an interface we can easily switch from datatable to some other calulator; useful for testing, or if we wanted to compare different calculators without much recoding
{
    T Calculate<T>(string expression) where T: struct;
}
class DataTableCalculator: ICalculator 
{
    readonly DataTable dataTable = new DataTable();
    public DataTableCalculator(){}
    public T Calculate<T>(string expression) where T: struct =>
        (T)dataTable.Compute(expression, null);
}

class Calculator: ICalculator
{
    static ICalculator internalInstance;
    public Calculator(){}
    public void InitialiseCalculator (ICalculator calculator)
    {
        if (internalInstance != null)
        {
            throw new InvalidOperationException("Calculator has already been initialised");
        }
        internalInstance = calculator;
    }
    public T Calculate<T>(string expression) where T: struct =>
        internalInstance.Calculate<T>(expression);
}

//then we use it on our code
void Main()
{
    var calculator1 = new Calculator();
    calculator1.InitialiseCalculator(new DataTableCalculator());
    var equation = "14 + 2 * 32 / 60 + 43 - 7 + 3 - 1 + 0 * 7 + 87 - 32 / 34"; 
    Console.WriteLine(calculator1.Calculate<double>(equation)); //139.125490196078
    equation = "1 + 2 - 3 + 4";
    Console.WriteLine(calculator1.Calculate<int>(equation)); //4
    calculator1 = null;
    System.GC.Collect(); //in reality we'd pretty much never do this, but just to illustrate that our static variable continues fro the life of the app domain rather than the life of the instance
    var calculator2 = new Calculator();
    //calculator2.InitialiseCalculator(new DataTableCalculator()); //uncomment this and you'll get an error; i.e. the calulator should only be initialised once.
    equation = "1 + 2 - 3 + 4 / 5 * 6 - 7 / 8 + 9";
    Console.WriteLine(calculator2.Calculate<double>(equation)); //12.925
}

注意:上述解决方案使用静态变量;有些人反对使用静力学。对于这种情况(即在应用程序的生命周期中,您只需要一种计算方式),这是一个合法的用例。如果您想支持在运行时切换计算器,则需要采用不同的方法。


测试和比较后更新

运行了一些性能测试:

  • DataTable.Compute 方法的最大问题是,对于您正在处理它的大小的方程,它会抛出 StackOverflowException; (即基于您的方程生成器的循环for (int i = 0; i &lt; 1000000; i++)
  • 对于具有较小方程 (i &lt; 1000) 的单个操作,计算方法(包括 double 结果上的构造函数和 Convert.ToInt32)花费的时间几乎是 100 倍。
  • 对于单个操作,我也经常遇到溢出异常;即因为操作的结果将值推到了支持的数据类型的范围之外......
  • 即使我们将构造函数/初始化调用移到计时区域之外并移除到 int 的转换(并运行数千次迭代以获得平均值),您的解决方案也比我的解决方案快 3.5 倍。

文档链接:https://msdn.microsoft.com/en-us/library/system.data.datatable.compute%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396

【讨论】:

  • 如果您不确定它是否更快,为什么要添加作为答案?
  • Compute 根据算子优先级的数学规则计算。根据 OP,这不起作用:var formula = "5 + 2 * 0"; 应该导致 0 而不是 5
  • 我严重怀疑这会更快...默认构造函数本身创建 4 个集合,一个 RecordManager 和一个 DataRowBuilder。
  • 参考你对MongZhu的回答:你有没有想到这可能是学习如何编码的任务,而不是得到实际数学上正确的结果?学会遵循违背你认为“正确”的要求是一个宝贵的教训,恕我直言。 (虽然我怀疑这是故意的)。
  • 好吧,我不得不承认,OP 中并没有明确说明它一个任务。这就是为什么我没有DV。我只是通过提出要求的方式来推断,这听起来很熟悉——就像我们在第一学期的作业(“当时”在 90 年代哈哈)
【解决方案4】:

更新

我最初的答案只是在深夜尝试将其放入unsafe 时有点有趣,但我惨遭失败(实际上根本没有工作而且速度较慢)。不过我决定再试一次。

前提是使所有内容都内联,尽可能多地删除IL,将所有内容保留在intchar* 中,并使我的代码漂亮。我通过移除开关进一步优化了这一点,Ifs 在这种情况下会更有效,我们也可以以最合乎逻辑的方式对它们进行排序。最后,如果我们删除对我们所做的事情的检查数量并假设输入是正确的,我们可以通过假设以下内容来消除更多的开销:如果char > '0' 它必须是一个数字。如果是空格我们可以做一些计算,否则一定是算子。

这是我最后一次尝试,将 10,000,000 次计算运行 100 次以获得平均值,每次测试都会执行 GC.Collect()GC.WaitForPendingFinalizers(),因此我们不会分割内存。

结果

Test                          : ms    : Cycles (rough) : Increase
-------------------------------------------------------------------
OriginalCalc                  : 1,295 : 4,407,795,584  :
MarcEvalNoSubStrings          :   241 :   820,660,220  : 437.34%, * 5.32
MarcEvalNoSubStringsUnsafe    :   206 :   701,980,373  : 528.64%, * 6.28
MiraiMannCalc1                :   225 :   765,678,062  : 475.55%, * 5.75
MiraiMannCalc2                :   183 :   623,384,924  : 607.65%, * 7.07
MyCalc4                       :   156 :   534,190,325  : 730.12%, * 8.30
MyCalc5                       :   146 :   496,185,459  : 786.98%, * 8.86
MyCalc6                       :   134 :   455,610,410  : 866.41%, * 9.66

目前最快的代码

unsafe int Calc6(ref string expression)
{
   int res = 0, val = 0, op = 0;
   var isOp = false;

   // pin the array
   fixed (char* p = expression)
   {
      // Let's not evaluate this 100 million times
      var max = p + expression.Length;

      // Let's go straight to the source and just increment the pointer
      for (var i = p; i < max; i++)
      {
         // numbers are the most common thing so let's do a loose
         // basic check for them and push them in to our val
         if (*i >= '0') { val = val * 10 + *i - 48; continue; }

         // The second most common thing are spaces
         if (*i == ' ')
         {
            // not every space we need to calculate
            if (!(isOp = !isOp)) continue;

            // In this case 4 ifs are more efficient then a switch
            // do the calculation, reset out val and jump out
            if (op == '+') { res += val; val = 0; continue; }
            if (op == '-') { res -= val; val = 0; continue; }
            if (op == '*') { res *= val; val = 0; continue; }
            if (op == '/') { res /= val; val = 0; continue; }

            // this is just for the first op
            res = val; val = 0; continue;                
         }
         // anything else is considered an operator
         op = *i;
      }

      if (op == '+') return res + val;
      if (op == '-') return res - val;
      if (op == '*') return res * val;
      if (op == '/') return res / val;

      throw new IndexOutOfRangeException();
   }
}

上一个

unsafe int Calc4(ref string expression)
{
   int res = 0, val = 0, op = 0;
   var isOp = false;

   fixed (char* p = expression)
   {
      var max = p + expression.Length;
      for (var i = p; i < max; i++)
         switch (*i)
         {               
            case ' ':
               isOp = !isOp;
               if (!isOp) continue;    
               switch (op)
               {
                  case '+': res += val; val = 0; continue;
                  case '-': res -= val; val = 0; continue;
                  case '*': res *= val; val = 0; continue;
                  case '/': res /= val; val = 0; continue;
                  default: res = val; val = 0;  continue;
               }
            case '+': case '-': case '*': case '/': op = *i; continue;
            default: val = val * 10 + *i - 48; continue;
         }

      switch (op)
      {
         case '+': return res + val;
         case '-': return res - val;
         case '*': return res * val;
         case '/': return res / val;
         default : return -1;
      }
   }
}

我如何测量线程周期

static class NativeMethods {
    public static ulong GetThreadCycles() {
        ulong cycles;
        if (!QueryThreadCycleTime(PseudoHandle, out cycles))
            throw new System.ComponentModel.Win32Exception();
        return cycles;
    }
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool QueryThreadCycleTime(IntPtr hThread, out ulong cycles);
    private static readonly IntPtr PseudoHandle = (IntPtr)(-2);

}

原帖

我想我会尝试变得聪明并使用固定并通过数百万次计算将其最大化

public static unsafe int Calc2(string sInput)
{
   var buf = "";
   var start = sInput.IndexOf(' ');
   var value1 = int.Parse(sInput.Substring(0, start));
   string op = null;
   var iResult = 0;
   var isOp = false;
   fixed (char* p = sInput)
   {
      for (var i = start + 1; i < sInput.Length; i++)
      {
         var cur = *(p + i);
         if (cur == ' ')
         {
            if (!isOp)
            {
               op = buf;
               isOp = true;
            }
            else
            {
               var value2 = int.Parse(buf);
               switch (op[0])
               {
                  case '+': iResult += value1 + value2; break;
                  case '-': iResult += value1 - value2; break;
                  case '*': iResult += value1 * value2; break;
                  case '/': iResult += value1 / value2; break;
               }

               value1 = value2;
               isOp = false;
            }

            buf = "";
         }
         else
         {
            buf += cur;
         }
      }
   }

   return iResult;
}

private static void Main(string[] args)
{
   var input = "14 + 2 * 32 / 60 + 43 - 7 + 3 - 1 + 0 * 7 + 87 - 32 / 34";
   var sb = new StringBuilder();
   sb.Append(input);
   for (var i = 0; i < 10000000; i++)
      sb.Append(" + " + input);

   var sw = new Stopwatch();
   sw.Start();

   Calc2(sb.ToString());

   sw.Stop();

   Console.WriteLine($"sw : {sw.Elapsed:c}");
}

结果比原来慢了 2 秒!

【讨论】:

  • IMO 你这里主要测试StringBuilder 的性能
  • @MarcGravell 是的,我今天没有免费午餐,这是肯定的
  • 我将破解我在问题/答案中可以看到的所有可行方法,并将它们全部放入单个 BenchmarkDotNet 跑步者中;这应该允许 OP 比较所有的想法
  • @MarcGravell 听起来像是一个计划
  • 完成;它与原版非常相似 - 可能是因为它仍然使用大量解析/子字符串
【解决方案5】:

这是一个 Java 有趣的事实。我在 Java 中实现了同样的东西,它的运行速度比 C# 中的 Mirai Mann 实现快了大约 20 倍。在我的机器上,100M 个字符的输入字符串大约需要 353 毫秒。

以下是创建和测试结果的代码。

另外,请注意,虽然它是一款出色的 Java/C# 性能测试器,但这并不是最佳解决方案。通过多线程可以实现更好的性能。可以计算字符串的各个部分,然后组合结果。

public class Test {

    public static void main(String...args){
        new Test().run();
    }

    private void run() {
        long startTime = System.currentTimeMillis();
        Random random = new Random(123);
        int result = 0;
        StringBuilder input = new StringBuilder();
        input.append(random.nextInt(99) + 1);
        while (input.length() < 100_000_000){
            int value = random.nextInt(100);
            switch (random.nextInt(4)){
                case 0:
                    input.append("-");
                    result -= value;
                    break;
                case 1: // +
                    input.append("+");
                    result += value;
                    break;
                case 2:
                    input.append("*");
                    result *= value;
                    break;
                case 3:
                    input.append("/");
                    while (value == 0){
                        value = random.nextInt(100);
                    }
                    result /= value;
                    break;
            }
            input.append(value);
        }
        String inputData = input.toString();
        System.out.println("Test created in " + (System.currentTimeMillis() - startTime));

        startTime = System.currentTimeMillis();
        int testResult = test(inputData);
        System.out.println("Completed in " + (System.currentTimeMillis() - startTime));

        if(result != testResult){
            throw new Error("Oops");
        }
    }

    private int test(String inputData) {
        char[] input;
        try {
            Field val = String.class.getDeclaredField("value");
            val.setAccessible(true);
            input = (char[]) val.get(inputData);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new Error(e);
        }
        int result = 0;
        int startingI = 1;
        {
            char c = input[0];
            if (c >= '0' && c <= '9') {
                result += c - '0';
                c = input[1];
                if (c >= '0' && c <= '9') {
                    result += (c - '0') * 10;
                    startingI++;
                }
            }
        }

        for (int i = startingI, length = input.length, value=0; i < length; i++) {
            char operation = input[i];
            i++;
            char c = input[i];
            if(c >= '0' && c <= '9'){
                value += c - '0';
                c = input[i + 1];
                if(c >= '0' && c <= '9'){
                    value = value * 10 + (c - '0');
                    i++;
                }
            }
            switch (operation){
                case '-':
                    result -= value;
                    break;
                case '+':
                    result += value;
                    break;
                case '*':
                    result *= value;
                    break;
                case '/':
                    result /= value;
                    break;
            }
            value = 0;
        }

        return result;
    }
}

当您阅读代码时,您会看到我在将字符串转换为 char 数组时使用了一个小技巧。我对字符串进行了变异,以避免为 char 数组分配额外的内存。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-31
    • 1970-01-01
    • 2023-03-13
    • 2014-09-28
    • 2020-07-22
    相关资源
    最近更新 更多