[英]What takes too long on this code?
試圖解決另一個SO 問題 ,我提出了以下算法,我認為這個算法非常優化。 然而,在所有解決方案上運行DotNetBenchmark
時,我感到非常驚訝的是,我的代碼運行時間平均為387 ms
相比之下,其他一些答案實現了~ 20-30 ms
。
[MethodImpl(MethodImplOptions.AggressiveInlining)]
int CalcMe(string input) // I used Marc Gravel's input generation method
{
var operands = input.Split(' ');
var j = 1; // operators index
var result = int.Parse(operands[0]); // output
// i = numbers index
for (int i = 2; i < operands.Length; i += 2)
{
switch (operands[j])
{
case "+":
result += int.Parse(operands[i]);
break;
case "-":
result -= int.Parse(operands[i]);
break;
case "*":
result *= int.Parse(operands[i]);
break;
case "/":
try
{
result /= int.Parse(operands[i]);
break;
}
catch
{
break; // division by 0.
}
default:
throw new Exception("Unknown Operator");
}
j += 2; // next operator
}
return result;
}
只需將String.Split()
提取到調用者Main()
方法,我String.Split()
執行速度降低到110 ms
,但由於所有其他答案都直接處理輸入,因此仍無法解決問題。
我只是想了解或許會改變我對優化的思考方式。 我看不到任何我只使用的關鍵字。 switch
, for
和int.Parse()
幾乎都是其他解決方案。
編輯1:測試輸入生成輸入生成從原始quetsion上的Marc answer復制,如下所示:
static string GenerateInput()
{
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));
return input.ToString();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
這里幾乎沒有任何成就。 當您想告訴編譯器只是將代碼復制並粘貼到多個位置以避免不必要的方法調用時,使用內聯。 在大多數情況下,知道什么時候自己做這件事真是太聰明了。
var operands = input.Split(' ');
使JIT遍歷整個字符串,進行搜索,拆分字符串並填充數組,這可能需要很長時間。
switch (operands[j])
打開字符串也會產生影響,因為它必須在案例上調用equals。 如果你正在查看性能(例如char),你想在switch中使用簡單類型。
int.Parse
這實際上做了一堆分配,甚至處理不安全的代碼。 您可以在此處查看解析代碼:
https://referencesource.microsoft.com/#mscorlib/system/number.cs,698
或者如果鏈接斷開:
[System.Security.SecuritySafeCritical] // auto-generated
internal unsafe static Int32 ParseInt32(String s, NumberStyles style, NumberFormatInfo info) {
Byte * numberBufferBytes = stackalloc Byte[NumberBuffer.NumberBufferBytes];
NumberBuffer number = new NumberBuffer(numberBufferBytes);
Int32 i = 0;
StringToNumber(s, style, ref number, info, false);
if ((style & NumberStyles.AllowHexSpecifier) != 0) {
if (!HexNumberToInt32(ref number, ref i)) {
throw new OverflowException(Environment.GetResourceString("Overflow_Int32"));
}
}
else {
if (!NumberToInt32(ref number, ref i)) {
throw new OverflowException(Environment.GetResourceString("Overflow_Int32"));
}
}
return i;
}
[System.Security.SecuritySafeCritical] // auto-generated
private unsafe static void StringToNumber(String str, NumberStyles options, ref NumberBuffer number, NumberFormatInfo info, Boolean parseDecimal) {
if (str == null) {
throw new ArgumentNullException("String");
}
Contract.EndContractBlock();
Contract.Assert(info != null, "");
fixed (char* stringPointer = str) {
char * p = stringPointer;
if (!ParseNumber(ref p, options, ref number, null, info , parseDecimal)
|| (p - stringPointer < str.Length && !TrailingZeros(str, (int)(p - stringPointer)))) {
throw new FormatException(Environment.GetResourceString("Format_InvalidString"));
}
}
}
我認為比較字符串比比較字符要復雜得多
下面是關鍵區別
switch (operands[j])
{
case "+":
...
switch (cOperator)
{
case '+':
...
有趣的問題! 我有興趣為自己實現這個,並檢查我能想出什么,以及它與其他實現的比較。 我在F#中做過,但由於F#和C#都是強類型的CLR語言,並且下面獲得的見解(可以說)獨立於C#,我希望你們同意以下內容並非完全偏離主題。
首先,我需要一些函數來創建一個合適的表達式字符串(根據您的發布改編),測量時間,並使用生成的字符串運行一堆函數:
module Testbed =
let private mkTestCase (n : int) =
let next (r : System.Random) i = r.Next (0, i)
let r = System.Random ()
let s = System.Text.StringBuilder n
let ops = "+-*/"
(s.Append (next r 100), {1 .. n})
||> Seq.fold (fun s _ ->
let nx = next r 100
let op = ops.[next r (if nx = 0 then 3 else 4)]
s.Append (" " + string op + " " + string nx))
|> string
let private stopwatch n f =
let mutable r = Unchecked.defaultof<_>
let sw = System.Diagnostics.Stopwatch ()
sw.Start ()
for i = 1 to n do r <- f ()
sw.Stop ()
(r, sw.ElapsedMilliseconds / int64 n)
let runtests tests =
let s, t = stopwatch 100 (fun () -> mkTestCase 1000000)
stdout.Write ("MKTESTCASE\nTime: {0}ms\n", t)
tests |> List.iter (fun (name : string, f) ->
let r, t = stopwatch 100 (fun () -> f s)
let w = "{0} ({1} chars)\nResult: {2}\nTime: {3}ms\n"
stdout.Write (w, name, s.Length, r, t))
對於100萬次操作(大約490萬個字符)的字符串, mkTestCase
函數在我的筆記本電腦上運行了317ms。
接下來,我將您的功能翻譯為F#:
module MethodsToTest =
let calc_MBD1 (s : string) =
let inline runop f a b =
match f with
| "+" -> a + b
| "-" -> a - b
| "*" -> a * b
| "/" -> a / b
| _ -> failwith "illegal op"
let rec loop (ops : string []) r i j =
if i >= ops.Length then r else
let n = int ops.[i]
loop ops (runop ops.[j] r n) (i + 2) (j + 2)
let ops = s.Split ' '
loop ops (int ops.[0]) 2 1
這在我的筆記本電腦上跑了488ms。
接下來我想檢查字符串匹配是否真的比字符匹配慢得多:
let calc_MBD2 (s : string) =
let inline runop f a b =
match f with
| '+' -> a + b
| '-' -> a - b
| '*' -> a * b
| '/' -> a / b
| _ -> failwith "illegal op"
let rec loop (ops : string []) r i j =
if i >= ops.Length then r else
let n = int ops.[i]
loop ops (runop ops.[j].[0] r n) (i + 2) (j + 2)
let ops = s.Split ' '
loop ops (int ops.[0]) 2 1
一般的智慧會說,字符匹配應該明顯更快,因為它只涉及原始比較而不是計算哈希,但上面在我的筆記本電腦上運行482ms,所以原始字符比較和比較字符串的哈希值之間的區別長度1幾乎可以忽略不計。
最后,我檢查了手動滾動數字解析是否會提供顯着的節省:
let calc_MBD3 (s : string) =
let inline getnum (c : char) = int c - 48
let parse (s : string) =
let rec ploop r i =
if i >= s.Length then r else
let c = s.[i]
let n = if c >= '0' && c <= '9'
then 10 * r + getnum c else r
ploop n (i + 1)
ploop 0 0
let inline runop f a b =
match f with
| '+' -> a + b
| '-' -> a - b
| '*' -> a * b
| '/' -> a / b
| _ -> failwith "illegal op"
let rec loop (ops : string []) r i j =
if i >= ops.Length then r else
let n = parse ops.[i]
loop ops (runop ops.[j].[0] r n) (i + 2) (j + 2)
let ops = s.Split ' '
loop ops (parse ops.[0]) 2 1
這在我的筆記本電腦上運行了361ms,因此保存很重要,但功能仍然比我自己的創建慢一個數量級(見下文),從而得出結論:初始字符串拆分占用了大部分時間。
為了比較,我還從您引用F#的帖子中翻譯了OP的功能:
let calc_OP (s : string) =
let operate r op x =
match op with
| '+' -> r + x
| '-' -> r - x
| '*' -> r * x
| '/' -> r / x
| _ -> failwith "illegal op"
let rec loop c n r =
if n = -1 then
operate r s.[c + 1] (int (s.Substring (c + 3)))
else
operate r s.[c + 1] (int (s.Substring (c + 3, n - (c + 2))))
|> loop n (s.IndexOf (' ', n + 4))
let c = s.IndexOf ' '
loop c (s.IndexOf (' ', c + 4)) (int (s.Substring (0, c)))
這在我的筆記本電腦上運行了238毫秒,因此使用子串並不像分割字符串那么慢,但它仍然遠非最佳。
最后是我自己的表達式解釋器實現,考慮到最快的處理方式是逐個字符地手動完成,只迭代字符串一次,以及堆分配(通過創建新對象,如字符串或數組)應該盡可能避免在循環內部:
let calc_Dumetrulo (s : string) =
let inline getnum (c : char) = int c - 48
let inline isnum c = c >= '0' && c <= '9'
let inline isop c =
c = '+' || c = '-' || c = '*' || c = '/'
let inline runop f a b =
match f with
| '+' -> a + b
| '-' -> a - b
| '*' -> a * b
| '/' -> a / b
| _ -> failwith "illegal op"
let rec parse i f a c =
if i >= s.Length then
if c = -1 then a else runop f a c
else
let k, j = s.[i], i + 1
if isnum k then
let n = if c = -1 then 0 else c
parse j f a (10 * n + getnum k)
elif isop k then parse j k a c
elif c = -1 then parse j f a c
else parse j f (runop f a c) -1
parse 0 '+' 0 -1
這在我的筆記本電腦上運行了28毫秒。 您可以在C#中以相同的方式表達,除了尾遞歸,它應該由for
或while
循環表示:
static int RunOp(char op, int a, int b)
{
switch (op)
{
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
case '/': return a / b;
default: throw new InvalidArgumentException("op");
}
}
static int Calc_Dumetrulo(string s)
{
int a = 0, c = -1;
char op = '+';
for (int i = 0; i < s.Length; i++)
{
char k = s[i];
if (k >= '0' && k <= '9')
c = (c == -1 ? 0 : 10 * c) + ((int)k - 48);
else if (k == '+' || k == '-' || k == '*' || k == '/')
op = k;
else if (c == -1) continue;
else
{
a = RunOp(op, a, c);
c = -1;
}
}
if (c != -1) a = RunOp(op, a, c);
return a;
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.