簡體   English   中英

for-loop / switch-statement的性能優化

[英]Performance optimization of for-loop / switch-statement

請幫我確定以下哪些是更優化的代碼?

for(int i=0;i<count;i++)
{
    switch(way)
    {
        case 1:
            doWork1(i);
            break;
        case 2:
            doWork2(i);
            break;
        case 3:
            doWork3(i);
            break;
    }
}

要么

switch(way)
{
    case 1:
        for(int i=0;i<count;i++)
        {
            doWork1(i);
        }
        break;
    case 2:
        for(int i=0;i<count;i++)
        {
            doWork2(i);
        }
        break;
    case 3:
        for(int i=0;i<count;i++)
        {
            doWork3(i);
        }
        break;
}

在第一種情況下, 總是在每次迭代中始終檢查開關情況條件的開銷。 在第二種情況下,開銷不存在。 我覺得第二種情況要好得多。 如果有人有任何其他解決方法,請幫我建議。

在低連續值上switch非常快的 - 這種類型的跳轉具有高度優化的處理。 坦率地說,你所要求的在絕大多數情況下都沒有任何區別 - doWork2(i); 任何東西 doWork2(i); 會淹沒這個; 哎呀,虛擬電話本身可能會淹沒它。

如果真的,真的,真的很重要(我很難想到這里真實的情況),那么:衡量它。 在任何明顯的情況下, 只有測量它的方法才是 實際的,精確的代碼 - 你不能概括微微優化。

所以:

  1. 沒關系
  2. 測量
  3. 沒關系

你可以這樣做:

Func(void, int> doWork;
switch(way) 
{ 
    case 1: 
        doWork = doWork1; 
        break; 
    case 2: 
        doWork = doWork2; 
        break; 
    case 3: 
        doWork = doWork3; 
        break; 
} 
for (int i=0;i<count;i++)  
{
     doWork(i);
}

(寫在這里,代碼可能不完全編譯,只是為了給你這個想法......)

我會問自己優化的問題

  1. 首先,數量有多大? 是1,2,10,10000000000?
  2. 機器運行代碼有多強大?
  3. 我應該寫更少的代碼嗎?
  4. 有人在我寫完之后會讀這段代碼嗎? 如果是這樣他有多專業?
  5. 我缺少什么? 時間? 速度? 別的什么?
  6. 什么way 我從哪里得到它? way為1或2或3的概率是多少?

很明顯,第一個代碼片段將用於切換部分,直到i達到計數但計數有多大? 如果它不是一個非常大的數字那么無關緊要? 如果它太大而你的運行時間非常慢那么它就沒用了。 但是,正如我所說,如果你想要可讀性並且可以保證計數很小,為什么不使用第一個呢? 它比第二個更容易讀取,而且我喜歡的代碼更少。

第二個片段,看起來很流暢但如果count是一個巨大的數字應該是首選。

實際上,盡管有一些評論,它可能會更快一些。

我們實際測試它:

using System;
using System.Diagnostics;

namespace Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            int count = 1000000000;

            Stopwatch sw = Stopwatch.StartNew();

            for (int way = 1; way <= 3; ++way)
                test1(count, way);

            var elapsed1 = sw.Elapsed;
            Console.WriteLine("test1() took " + elapsed1);

            sw.Restart();

            for (int way = 1; way <= 3; ++way)
                test2(count, way);

            var elapsed2 = sw.Elapsed;
            Console.WriteLine("test2() took " + elapsed2);

            Console.WriteLine("test2() was {0:f1} times as fast.", + ((double)elapsed1.Ticks)/elapsed2.Ticks);
        }

        static void test1(int count, int way)
        {
            for (int i = 0; i < count; ++i)
            {
                switch (way)
                {
                    case 1: doWork1(); break;
                    case 2: doWork2(); break;
                    case 3: doWork3(); break;
                }
            }
        }

        static void test2(int count, int way)
        {
            switch (way)
            {
                case 1:
                    for (int i = 0; i < count; ++i)
                        doWork1();
                    break;

                case 2:
                    for (int i = 0; i < count; ++i)
                        doWork2();
                    break;

                case 3:
                    for (int i = 0; i < count; ++i)
                        doWork3();
                    break;
            }
        }

        static void doWork1()
        {
        }

        static void doWork2()
        {
        }

        static void doWork3()
        {
        }
    }
}

現在這是非常不現實的,因為doWork()方法不做任何事情。 但是,它會給我們一個基線時間。

我在Windows 7 x64系統上構建RELEASE的結果是:

test1() took 00:00:03.8041522
test2() took 00:00:01.7916698
test2() was 2.1 times as fast.

因此,將循環移動到switch語句中會使其快速超過兩次。

現在讓我們通過在doWork()中添加一些代碼來使它變得更加真實:

using System;
using System.Diagnostics;

namespace Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            int count = 1000000000;

            Stopwatch sw = Stopwatch.StartNew();

            for (int way = 1; way <= 3; ++way)
                test1(count, way);

            var elapsed1 = sw.Elapsed;
            Console.WriteLine("test1() took " + elapsed1);

            sw.Restart();

            for (int way = 1; way <= 3; ++way)
                test2(count, way);

            var elapsed2 = sw.Elapsed;
            Console.WriteLine("test2() took " + elapsed2);

            Console.WriteLine("test2() was {0:f1} times as fast.", + ((double)elapsed1.Ticks)/elapsed2.Ticks);
        }

        static int test1(int count, int way)
        {
            int total1 = 0, total2 = 0, total3 = 0;

            for (int i = 0; i < count; ++i)
            {
                switch (way)
                {
                    case 1: doWork1(i, ref total1); break;
                    case 2: doWork2(i, ref total2); break;
                    case 3: doWork3(i, ref total3); break;
                }
            }

            return total1 + total2 + total3;
        }

        static int test2(int count, int way)
        {
            int total1 = 0, total2 = 0, total3 = 0;

            switch (way)
            {
                case 1:
                    for (int i = 0; i < count; ++i)
                        doWork1(i, ref total1);
                    break;

                case 2:
                    for (int i = 0; i < count; ++i)
                        doWork2(i, ref total2);
                    break;

                case 3:
                    for (int i = 0; i < count; ++i)
                        doWork3(i, ref total3);
                    break;
            }

            return total1 + total2 + total3;
        }

        static void doWork1(int n, ref int total)
        {
            total += n;
        }

        static void doWork2(int n, ref int total)
        {
            total += n;
        }

        static void doWork3(int n, ref int total)
        {
            total += n;
        }
    }
}

現在我得到了這些結果:

test1() took 00:00:03.9153776
test2() took 00:00:05.3220507
test2() was 0.7 times as fast.

現在將循環放入開關是SLOWER! 這種違反直覺的結果是典型的這類事情,並說明了在嘗試優化代碼時應始終執行時序測試的原因。 (並且優化這樣的代碼通常是你甚至不應該做的事情,除非你有充分的理由懷疑存在瓶頸。你最好花時間清理你的代碼。))

我做了一些其他測試,對於稍微簡單的doWork()方法,test2()方法更快。 它實際上很大程度上取決於JIT編譯器可以對優化做些什么。

注意:我認為我的第二個測試代碼的速度差異的原因是因為JIT編譯器在內聯調用doWork()時可以優化掉'ref'調用,當它們不在循環中時,就像在test1()中一樣; 而對於test2(),它不能(由於某種原因)。

您應該測量它以確定是否值得優化(我非常確定它不是 )。 就個人而言,我更喜歡第一個用於可讀性和簡潔性(代碼少,不易出錯,更“ ”)。

這是另一種更簡潔的方法:

for(int i = 0; i < count; i++)
{
    doAllWays(way, i); // let the method decide what to do next
}

所有“方式”似乎都得到了解決,否則它們就不會出現在同一個switch 因此,首先將它們捆綁在一個方法中進行switch是有意義的。

第二種方法更有效; 無論如何,你必須完成完整的for循環。 但是在第一種方法中,你不必要地重復case語句計數次數。

假設你在這里遇到性能問題(因為在大多數情況下,交換機真的非常快):

如果您對switch語句感到困擾,我建議您在此處應用重構。

可以很容易地用策略模式替換開關(因為在for循環中沒有改變切換值,所以根本不需要切換)。

真正的優化目標是那些循環,但沒有上下文,很難說明可以做些什么。

這里有一些關於重構開關的更多信息(例如,關於策略模式) 關於重構開關的CodeProject文章

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM