Самый быстрый способ решения цепных вычислений

У меня есть вход вроде

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 * 32 есть 512
  • Возможные вычисления: +-*/
  • деление на 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");
    }
}

Мне нужен быстрый способ получить результат.

Вопрос: есть ли способ сделать этот расчет быстрее? У меня несколько потоков, но я использую только один.

Update:

Test-case: (я удалил деление на 0 ошибкой и удалил StringBuilder.ToString() из измерения StopWatch)

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();

Ответ 1

Следующее решение - конечный автомат. Calc (вход) = O (n). Для лучшей производительности это решение не использует IndexOf, Substring, Parse, конкатенацию строк или повторное считывание значения (выборка 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;
    }

Результаты в абстрактных точках:

  • Calculate1 230
  • Calculate2 192
  • Calculate3 111

Ответ 2

Редактирование редактирования: обновлено с помощью последних версий The General и Mirai Mann:

Если вы хотите узнать, какая лошадь самая быстрая: расы лошадей. Вот результаты BenchmarkDotNet, сравнивающие различные ответы на этот вопрос (I не имеют, объединили свой код в мой полный пример, потому что это неправильно - только цифры представлены) с повторяемым, но большим случайным вводом, через

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), 32bit 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:

Если бы я пытался это сделать, я был бы соблазнен, чтобы посмотреть на работу Span<T> в 2.1 предварительных просмотрах - точка в том, что a Span<T> можно нарезать без выделения (и a 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;
    }
}

Ответ 3

Примечание

В комментариях этот ответ не дает эффективного решения. Я оставлю его здесь, так как есть моменты, которые следует учитывать/которые могут представлять интерес для других, которые в настоящее время ищут эту тему; но, как говорят люди ниже, это далеко не самое эффективное решение.


Оригинальный ответ

Структура .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

Исходная обратная связь на основе комментариев

В потоке комментариев мне нужно указать на некоторые вещи:

Это работает так, как я описал?

Нет; это следует за нормальными правилами математики.

Я предполагаю, что ваши измененные правила упрощают писать код для их обработки, а не потому, что вы хотите поддержать новую ветвь математики? Если это так, я бы возразил против этого. Люди будут ожидать, что вещи будут вести себя определенным образом; поэтому вы должны убедиться, что кто-либо, отправляющий уравнения в ваш код, был загружен знаниями, чтобы ожидать правил этой новой математики, а не использовать их существующие ожидания.

Здесь нет возможности изменить правила; поэтому, если ваше требование - изменить правила математики, это не сработает для вас.

Является ли это самым быстрым решением

Нет. Однако он должен хорошо работать, учитывая, что 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 < 1000000; i++).
  • Для одной операции с меньшим уравнением (i < 1000) метод вычисления (включая конструктор и Convert.ToInt32 результата double) занимает почти 100 раз дольше.
  • для одной операции я также чаще встречался с исключениями переполнения; то есть из-за того, что результат операций вытолкнул значение за пределы поддерживаемых типов данных...
  • Даже если мы переместим вызов конструктора/инициализации за пределы временной области и удалим преобразование в int (и запустим для тысяч итераций, чтобы получить среднее значение), ваше решение приходит в 3,5 раза быстрее, чем мое.

Ссылка на документы: https://msdn.microsoft.com/en-us/library/system.data.datatable.compute%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396

Ответ 4

Update

Моим первоначальным ответом было немного весело поздно вечером, пытаясь поместить это в unsafe, и я потерпел неудачу (фактически не работал вообще и был медленнее). Однако я решил сделать еще один выстрел

Предпосылка заключалась в том, чтобы сделать все встроенным, чтобы удалить как можно больше IL, сохранить все в int или char* и сделать мой код симпатичным. Я еще больше оптимизировал это, удалив коммутатор, Ifs будет полезен для руды в этой ситуации, также мы можем упорядочить их самым логичным образом. И, наконец, если мы удалим количество проверок для вещей, которые мы делаем, и предположим, что ввод верен, мы можем удалить еще больше накладных расходов, просто приняв такие вещи; если char is > '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)
   {
      // Lets not evaluate this 100 million times
      var max = p + expression.Length;

      // lets go straight to the source and just increment the pointer
      for (var i = p; i < max; i++)
      {
         // numbers are the most common thing so lets 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);

}

Оригинальное сообщение

Я думал, что id пытается быть умным и использовать фиксированное:/и max это с миллионами вычислений

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 секунды медленнее оригинала!

Ответ 5

Вот забавный факт Java. Я реализовал то же самое в Java, и он работает примерно в 20 раз быстрее, чем Mirai Mann в С#. На моей машине строка ввода символов 100M заняла около 353 миллисекунд.

Ниже приведен код, который создает и проверяет результат.

Кроме того, обратите внимание, что, хотя он хороший тестер производительности Java/С#, это не оптимальное решение. Лучшая производительность может быть достигнута путем многопоточности. Можно рассчитать части строки и затем объединить результат.

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.