C # 'da küçük kod örneklerinin karşılaştırılması, bu uygulama iyileştirilebilir mi?


104

Çoğu zaman SO'da hangi uygulamanın en hızlı olduğunu görmek için kendimi küçük kod parçalarını karşılaştırırken buluyorum.

Sıklıkla kıyaslama kodunun jitting veya çöp toplayıcıyı hesaba katmadığı şeklinde yorumlar görüyorum.

Yavaş yavaş geliştirdiğim aşağıdaki basit kıyaslama işlevine sahibim:

  static void Profile(string description, int iterations, Action func) {
        // warm up 
        func();
        // clean up
        GC.Collect();

        var watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < iterations; i++) {
            func();
        }
        watch.Stop();
        Console.Write(description);
        Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds);
    }

Kullanım:

Profile("a descriptions", how_many_iterations_to_run, () =>
{
   // ... code being profiled
});

Bu uygulamanın herhangi bir kusuru var mı? X uygulamasının Y uygulamasından Z üzerinde yinelemelerden daha hızlı olduğunu göstermek yeterince iyi mi? Bunu iyileştirmek için herhangi bir yol düşünebiliyor musun?

DÜZENLEME Zamana dayalı bir yaklaşımın (yinelemelerin aksine) tercih edildiği oldukça açık, herhangi birinin zaman kontrollerinin performansı etkilemediği herhangi bir uygulaması var mı?


Ayrıca BenchmarkDotNet'e bakın .
Ben Hutchison

Yanıtlar:


95

İşte değiştirilmiş işlev: Topluluk tarafından önerildiği gibi, bunu bir topluluk wiki'si olarak değiştirmekten çekinmeyin.

static double Profile(string description, int iterations, Action func) {
    //Run at highest priority to minimize fluctuations caused by other processes/threads
    Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
    Thread.CurrentThread.Priority = ThreadPriority.Highest;

    // warm up 
    func();

    var watch = new Stopwatch(); 

    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
    return watch.Elapsed.TotalMilliseconds;
}

Optimizasyonların etkinleştirildiği Sürümde derlediğinizden ve testleri Visual Studio dışında çalıştırdığınızdan emin olun . Bu son kısım önemlidir çünkü JIT, Release modunda bile bir hata ayıklayıcı eklenmiş olarak optimizasyonlarını belirler.


Döngü ek yükünü en aza indirmek için döngüyü 10 gibi birkaç kez açmak isteyebilirsiniz.
Mike Dunlavey

2
Stopwatch.StartNew'i kullanmak için yeni güncelledim. İşlevsel bir değişiklik değildir, ancak bir satır kod kaydeder.
LukeH

1
@Luke, harika değişim (Keşke + 1'leyebilseydim). @Mike emin değilim, sanal arama ek yükünün karşılaştırma ve atamadan çok daha yüksek olacağından şüpheleniyorum, bu nedenle performans farkı göz ardı edilebilir
Sam Saffron

Yineleme sayımını eyleme geçirmenizi ve orada döngü oluşturmanızı öneririm (muhtemelen - hatta kaydedilmemiş). Nispeten kısa bir işlemi ölçüyorsanız, bu tek seçenektir. Ve ters metrik görmeyi tercih ederim - örneğin geçiş sayısı / saniye.
Alex Yakunin

2
Ortalama zamanı gösterme hakkında ne düşünüyorsunuz? Bunun gibi bir şey: Console.WriteLine ("Geçen Ortalama Süre {0} ms", watch.ElapsedMilliseconds / iterations);
rudimenter

22

İadelerden önce kesinleştirme mutlaka tamamlanmayacaktır GC.Collect. Sonlandırma sıraya alınır ve ardından ayrı bir iş parçacığında çalıştırılır. Bu konu testleriniz sırasında hala aktif olabilir ve sonuçları etkileyebilir.

Testlerinize başlamadan önce sonlandırmanın tamamlandığından emin olmak istiyorsanız, arama yapmak isteyebilirsiniz GC.WaitForPendingFinalizers; bu, sonlandırma kuyruğu temizlenene kadar engellenecektir:

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

10
Neden bir GC.Collect()kez daha?
colinfang

7
@colinfang Çünkü "sonlandırılan" nesneler, sonlandırıcı tarafından GC'lenmez. Yani ikincisi Collect, "nihai hale getirilmiş" nesnelerin de toplandığından emin olmak içindir.
MAV

15

GC etkileşimlerini denklemden çıkarmak istiyorsanız , daha önce değil, GC.Collect çağrısından sonra 'ısınma' çağrınızı çalıştırmak isteyebilirsiniz . Bu şekilde .NET'in, işlevinizin çalışma kümesi için işletim sisteminden ayrılmış yeterli belleğe sahip olduğunu bilirsiniz.

Her yineleme için satır içi olmayan bir yöntem çağrısı yaptığınızı unutmayın, bu nedenle test ettiğiniz şeyleri boş bir gövdeyle karşılaştırdığınızdan emin olun. Ayrıca, bir yöntem çağrısından birkaç kat daha uzun olan şeyleri güvenilir bir şekilde zamanlayabileceğinizi de kabul etmeniz gerekir.

Ayrıca, ne tür şeylerin profilini çıkardığınıza bağlı olarak, zamanlamanızı belirli sayıda yineleme yerine belirli bir süre için çalıştırmak isteyebilirsiniz - bu, olmadan daha kolay karşılaştırılabilir sayılara yol açma eğiliminde olabilir. en iyi uygulama için çok kısa ve / veya en kötüsü için çok uzun bir süreye sahip olmak.


1
iyi noktalar, aklınızda zamana dayalı bir uygulama var mı?
Sam Saffron

6

Temsilciyi kesinlikle geçmekten kaçınırdım:

  1. Temsilci çağrısı ~ sanal yöntem çağrısıdır. Ucuz değil: .NET'teki en küçük bellek dağılımının ~% 25'i. Ayrıntılarla ilgileniyorsanız, örneğin bu bağlantıya bakın .
  2. İsimsiz temsilciler, sizin fark etmeyeceğiniz kapanışların kullanılmasına neden olabilir. Yine, kapanış alanlarına erişim, örneğin yığın üzerindeki bir değişkene erişmekten fark edilir şekilde gerçekleşir.

Kapatma kullanımına yol açan örnek bir kod:

public void Test()
{
  int someNumber = 1;
  Profiler.Profile("Closure access", 1000000, 
    () => someNumber + someNumber);
}

Kapanışların farkında değilseniz, .NET Reflector'da bu yönteme bir göz atın.


İlginç noktalar, ancak bir temsilciyi geçemezseniz yeniden kullanılabilir bir Profile () yöntemini nasıl oluşturursunuz? Rasgele kodu bir yönteme geçirmenin başka yolları var mı?
Ash

1
"Kullanarak (yeni Ölçüm (...)) {... ölçülen kod ...}" kullanıyoruz. Böylece, delegeyi geçmek yerine IDisposable uygulayan Ölçüm nesnesi elde ederiz. Bkz code.google.com/p/dataobjectsdotnet/source/browse/Xtensive.Core/...
Alex Yakunin

Bu, kapanışlarla ilgili herhangi bir soruna yol açmayacaktır.
Alex Yakunin

3
@AlexYakunin: Bağlantınız bozuk görünüyor. Cevabınıza Ölçü sınıfı kodunu ekleyebilir misiniz? Bunu nasıl uygularsanız uygulayın, bu IDisposable yaklaşımla birden çok kez profili oluşturulacak kodu çalıştıramayacağınızdan şüpheleniyorum. Bununla birlikte, karmaşık (iç içe geçmiş) bir uygulamanın farklı bölümlerinin nasıl performans gösterdiğini ölçmek istediğiniz durumlarda, ölçümlerin yanlış ve farklı zamanlarda çalıştırıldığında tutarsız olabileceğini aklınızda bulundurduğunuz sürece gerçekten çok kullanışlıdır. Projelerimin çoğunda aynı yaklaşımı kullanıyorum.
ShdNx

1
Performans testini birkaç kez çalıştırma gereksinimi gerçekten önemli (ısınma + çoklu ölçümler), bu yüzden temsilciyle de bir yaklaşıma geçtim. Üstelik, kapanışları kullanmazsanız, temsilci çağrısı, durumunda arabirim yöntemi çağrısından daha hızlıdır IDisposable.
Alex Yakunin

6

Bunun gibi kıyaslama yöntemleriyle üstesinden gelinmesi en zor sorunun uç durumları ve beklenmedik durumları hesaba katmak olduğunu düşünüyorum. Örneğin - "İki kod parçacığı yüksek CPU yükü / ağ kullanımı / disk atma / vb. Altında nasıl çalışır?" Belirli bir algoritmanın diğerinden önemli ölçüde daha hızlı çalışıp çalışmadığını görmek için temel mantık kontrolleri için harikadırlar . Ancak çoğu kod performansını doğru bir şekilde test etmek için, söz konusu kodun belirli darboğazlarını ölçen bir test oluşturmanız gerekir.

Yine de, küçük kod bloklarını test etmenin genellikle çok az yatırım getirisi olduğunu ve bakımı basit kod yerine aşırı karmaşık kodların kullanılmasını teşvik edebileceğini söyleyebilirim. Diğer geliştiricilerin veya kendimden 6 ay sonra hızlı bir şekilde anlayabileceği açık kod yazmak, yüksek düzeyde optimize edilmiş koddan daha fazla performans avantajına sahip olacaktır.


1
önemli olan, gerçekten yüklü olan bu terimlerden biridir. bazen% 20 daha hızlı bir uygulamaya sahip olmak önemlidir, bazen anlamlı olması için 100 kat daha hızlı olması gerekir. Anlaşılırlık konusunda sizinle hemfikir olun: stackoverflow.com/questions/1018407/…
Sam Saffron

Bu durumda önemli olan her şey o kadar yüklü değildir. Bir veya daha fazla eşzamanlı uygulamayı karşılaştırıyorsunuz ve bu iki uygulamanın performansındaki fark istatistiksel olarak önemli değilse, daha karmaşık yöntemi taahhüt etmeye değmez.
Paul Alexander

5

Ben bunu func()sadece bir, sıcak-up için birkaç kez.


1
Amaç, jit derlemesinin gerçekleştirilmesini sağlamaktı, ölçümden önce func'u birden çok kez çağırmanın avantajı ne olur?
Sam Saffron

3
JIT'e ilk sonuçlarını iyileştirme şansı vermek.
Alexey Romanov

1
.NET JIT zamanla sonuçlarını iyileştirmez (Java'nın yaptığı gibi). Bir yöntemi IL'den Assembly'ye yalnızca bir kez, ilk çağrıda dönüştürür.
Matt Warren

4

İyileştirme için öneri

  1. Yürütme ortamının kıyaslama için iyi olup olmadığını algılama (örneğin, bir hata ayıklayıcının eklenip eklenmediğini veya yanlış ölçümlere neden olacak şekilde jit optimizasyonunun devre dışı bırakılıp bırakılmadığını algılama).

  2. Kodun parçalarını bağımsız olarak ölçmek (darboğazın tam olarak nerede olduğunu görmek için).

  3. Farklı kod sürümlerini / bileşenlerini / parçalarını karşılaştırmak (İlk cümlenizde '... hangi uygulamanın en hızlı olduğunu görmek için küçük kod parçalarını kıyaslama' diyorsunuz).

# 1 ile ilgili olarak:

  • Bir hata ayıklayıcının eklenip eklenmediğini tespit etmek için özelliği okuyun System.Diagnostics.Debugger.IsAttached(Hata ayıklayıcının başlangıçta eklenmediği, ancak bir süre sonra eklendiği durumu da işlemeyi unutmayın).

  • Jit optimizasyonunun devre dışı bırakılıp bırakılmadığını tespit etmek DebuggableAttribute.IsJITOptimizerDisablediçin ilgili derlemelerin özelliklerini okuyun :

    private bool IsJitOptimizerDisabled(Assembly assembly)
    {
        return assembly.GetCustomAttributes(typeof (DebuggableAttribute), false)
            .Select(customAttribute => (DebuggableAttribute) customAttribute)
            .Any(attribute => attribute.IsJITOptimizerDisabled);
    }

# 2 ile ilgili olarak:

Bu pek çok şekilde yapılabilir. Bunun bir yolu, birkaç delegenin tedarik edilmesine izin vermek ve ardından bu delegeleri ayrı ayrı ölçmektir.

# 3 ile ilgili olarak:

Bu aynı zamanda birçok yolla da yapılabilir ve farklı kullanım durumları çok farklı çözümler gerektirir. Kıyaslama manuel olarak başlatılırsa, konsola yazmak iyi olabilir. Bununla birlikte, kıyaslama, yapı sistemi tarafından otomatik olarak gerçekleştirilirse, konsola yazmak muhtemelen o kadar iyi değildir.

Bunu yapmanın bir yolu, kıyaslama sonucunu, farklı bağlamlarda kolayca tüketilebilen güçlü bir şekilde yazılmış bir nesne olarak döndürmektir.


Etimo. Ölçütler

Diğer bir yaklaşım, kıyaslamaları gerçekleştirmek için mevcut bir bileşeni kullanmaktır. Aslında, şirketimde karşılaştırma aracımızı kamuya açık hale getirmeye karar verdik. Özünde, tıpkı buradaki diğer bazı cevapların önerdiği gibi, çöp toplayıcıyı, titreşimi, ısınmaları vb. Yönetir. Aynı zamanda yukarıda önerdiğim üç özelliğe de sahiptir. Eric Lippert blogunda tartışılan birçok konuyu yönetir .

Bu, iki bileşenin karşılaştırıldığı ve sonuçların konsola yazıldığı örnek bir çıktıdır. Bu durumda, karşılaştırılan iki bileşen 'KeyedCollection' ve 'MultiplyIndexedKeyedCollection' olarak adlandırılır:

Etimo.Benchmarks - Örnek Konsol Çıktısı

Bir NuGet paketi , örnek bir NuGet paketi ve kaynak kodu GitHub'da mevcuttur . Bir blog yazısı da var .

Aceleniz varsa, örnek paketi almanızı ve örnek delegeleri gerektiği gibi değiştirmenizi öneririm. Aceleniz yoksa, ayrıntıları anlamak için blog gönderisini okumak iyi bir fikir olabilir.


1

Ayrıca, JIT derleyicisinin kodunuzu karıştırmak için harcadığı zamanı hariç tutmak için gerçek ölçümden önce bir "ısınma" geçişi çalıştırmalısınız.


ölçümden önce yapılır
Sam Saffron

1

Kıyasladığınız koda ve üzerinde çalıştığı platforma bağlı olarak, kod hizalamasının performansı nasıl etkilediğini hesaba katmanız gerekebilir . Bunu yapmak için muhtemelen testi birden çok kez çalıştıran bir dış sarmalayıcı (ayrı uygulama alanlarında veya işlemlerde mi?), Bazen kodun oluşmasına neden olmak için JIT derlenmesini zorlamak için bazen ilk olarak "doldurma kodunu" çağıran bir dış sarmalayıcı gerekir. farklı şekilde hizalanmak üzere kıyaslandı. Tam bir test sonucu, çeşitli kod hizalamaları için en iyi durum ve en kötü durum zamanlamalarını verecektir.


1

Karşılaştırmalı değerlendirmeden Çöp Toplama etkisini ortadan kaldırmaya çalışıyorsanız, ayarlamaya değer GCSettings.LatencyModemi?

Değilse ve oluşturulan çöpün etkisinin funckıyaslamanın bir parçası olmasını istiyorsanız, o zaman testin sonunda (zamanlayıcının içinde) toplamayı da zorlamanız gerekmez mi?


0

Sorunuzla ilgili temel sorun, tek bir ölçümün tüm sorularınızı yanıtlayabileceği varsayımıdır. Durumun etkili bir resmini elde etmek için ve özellikle C # gibi çöp toplanmış bir dilde birden çok kez ölçüm yapmanız gerekir.

Başka bir cevap, temel performansı ölçmenin iyi bir yolunu verir.

static void Profile(string description, int iterations, Action func) {
    // warm up 
    func();

    var watch = new Stopwatch(); 

    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}

Ancak, bu tek ölçüm çöp toplamayı hesaba katmaz. Uygun bir profil ayrıca, birçok çağrıya yayılan çöp toplamanın en kötü durum performansını da hesaba katar (bu sayı, VM artık çöpleri hiç toplamadan sona erdirebildiğinden ancak yine de iki farklı uygulamayı karşılaştırmak için yararlıdır func.)

static void ProfileGarbageMany(string description, int iterations, Action func) {
    // warm up 
    func();

    var watch = new Stopwatch(); 

    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();
    }
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}

Ayrıca, yalnızca bir kez çağrılan bir yöntem için çöp toplamanın en kötü durum performansını ölçmek de isteyebilir.

static void ProfileGarbage(string description, int iterations, Action func) {
    // warm up 
    func();

    var watch = new Stopwatch(); 

    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}

Ancak, profile herhangi bir olası ek ölçüm önermekten daha önemli olan, birinin birden fazla farklı istatistiği ölçmesi gerektiği fikridir ve yalnızca bir tür istatistiği değil.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.