Bir yöntem için bellek vs performans hızı ne zaman optimize etmek?


107

Geçenlerde Amazon'da röportaj yaptım. Bir kodlama oturumu sırasında görüşmeci neden bir yöntemde bir değişken tanımladığımı sordu. Sürecimi açıkladım ve aynı sorunu daha az değişkenle çözmem için beni zorladı. Örneğin, ile başladı (bu görüşmeden değildi) Yöntem A daha sonra geliştirilmiş için , Yöntem B kaldırarak int s. Memnun kaldığını ve bunun bu yöntemle hafıza kullanımını azaltacağını söyledi.

Arkasındaki mantığı anlıyorum ama sorum şu:

Metot A ile Metot B'nin ne zaman ve ne zaman kullanılması uygundur?

A Yöntemininint s bildirildiğinden beri daha yüksek bellek kullanımına sahip olacağını göreceksiniz , ancak yalnızca bir hesaplama yapması gerekiyor, yani a + b. Öte yandan, B Yöntemi düşük bellek kullanımına sahiptir, ancak iki hesaplama yapmak zorundadır, yani a + biki kere. Bir tekniği diğerine ne zaman kullanırım? Veya, diğerlerinden her zaman tercih edilen tekniklerden biri mi? İki yöntemi değerlendirirken dikkate alınması gerekenler nelerdir?

Yöntem A:

private bool IsSumInRange(int a, int b)
{
    int s = a + b;

    if (s > 1000 || s < -1000) return false;
    else return true;
}

B Yöntemi:

private bool IsSumInRange(int a, int b)
{
    if (a + b > 1000 || a + b < -1000) return false;
    else return true;
}

229
Her iki durumda da modern bir derleyicinin aynı montajı yapacağına bahse girerim.
26

12
Düzenlemeniz cevabımı geçersiz kıldığı için soruyu orijinal durumuna geri döndürdüm - lütfen bunu yapma! Kodunuzu nasıl geliştireceğinizi bir soru sorarsanız, kodu gösterilen şekilde geliştirerek soruyu değiştirmeyin - bu, cevapların anlamsız görünmesini sağlar.
Doktor Brown

76
Bir saniye bekle, int süst ve alt sınırlar için bu sihirli sayılarla tamamen iyi olurken kurtulmak istediler ?
boş

34
Unutmayın: optimize etmeden önce profil. Modern derleyicilerde, Yöntem A ve Yöntem B aynı koda göre optimize edilebilir (daha yüksek optimizasyon seviyeleri kullanarak). Ayrıca, modern işlemcilerle, tek bir işlemde eklemekten daha fazlasını yapan talimatlara sahip olabilirler.
Thomas Matthews,

142
ne; okunabilirlik için optimize et.
Andy

Yanıtlar:


148

Neyin olabileceği veya olamayacağı hakkında spekülasyon yapmak yerine, sadece bakalım, olur mu? Ben (gerçi kullanışlı bir C # derleyicisi olmadığı için C ++ kullanmanız gerekecek C # örneğe bakın den VisualMelon ), ama aynı ilkeler bakılmaksızın geçerlidir eminim.

Röportajda karşılaştığınız iki alternatifi dahil edeceğiz. Ayrıca absbazı cevapların önerdiği şekilde kullanan bir sürüm de ekleyeceğiz .

#include <cstdlib>

bool IsSumInRangeWithVar(int a, int b)
{
    int s = a + b;

    if (s > 1000 || s < -1000) return false;
    else return true;
}

bool IsSumInRangeWithoutVar(int a, int b)
{
    if (a + b > 1000 || a + b < -1000) return false;
    else return true;
}

bool IsSumInRangeSuperOptimized(int a, int b) {
    return (abs(a + b) < 1000);
}

Şimdi, hiçbir optimizasyon olmadan derleyin: g++ -c -o test.o test.cpp

Şimdi bunun neyi ürettiğini tam olarak görebiliyoruz: objdump -d test.o

0000000000000000 <_Z19IsSumInRangeWithVarii>:
   0:   55                      push   %rbp              # begin a call frame
   1:   48 89 e5                mov    %rsp,%rbp
   4:   89 7d ec                mov    %edi,-0x14(%rbp)  # save first argument (a) on stack
   7:   89 75 e8                mov    %esi,-0x18(%rbp)  # save b on stack
   a:   8b 55 ec                mov    -0x14(%rbp),%edx  # load a and b into edx
   d:   8b 45 e8                mov    -0x18(%rbp),%eax  # load b into eax
  10:   01 d0                   add    %edx,%eax         # add a and b
  12:   89 45 fc                mov    %eax,-0x4(%rbp)   # save result as s on stack
  15:   81 7d fc e8 03 00 00    cmpl   $0x3e8,-0x4(%rbp) # compare s to 1000
  1c:   7f 09                   jg     27                # jump to 27 if it's greater
  1e:   81 7d fc 18 fc ff ff    cmpl   $0xfffffc18,-0x4(%rbp) # compare s to -1000
  25:   7d 07                   jge    2e                # jump to 2e if it's greater or equal
  27:   b8 00 00 00 00          mov    $0x0,%eax         # put 0 (false) in eax, which will be the return value
  2c:   eb 05                   jmp    33 <_Z19IsSumInRangeWithVarii+0x33>
  2e:   b8 01 00 00 00          mov    $0x1,%eax         # put 1 (true) in eax
  33:   5d                      pop    %rbp
  34:   c3                      retq

0000000000000035 <_Z22IsSumInRangeWithoutVarii>:
  35:   55                      push   %rbp
  36:   48 89 e5                mov    %rsp,%rbp
  39:   89 7d fc                mov    %edi,-0x4(%rbp)
  3c:   89 75 f8                mov    %esi,-0x8(%rbp)
  3f:   8b 55 fc                mov    -0x4(%rbp),%edx
  42:   8b 45 f8                mov    -0x8(%rbp),%eax  # same as before
  45:   01 d0                   add    %edx,%eax
  # note: unlike other implementation, result is not saved
  47:   3d e8 03 00 00          cmp    $0x3e8,%eax      # compare to 1000
  4c:   7f 0f                   jg     5d <_Z22IsSumInRangeWithoutVarii+0x28>
  4e:   8b 55 fc                mov    -0x4(%rbp),%edx  # since s wasn't saved, load a and b from the stack again
  51:   8b 45 f8                mov    -0x8(%rbp),%eax
  54:   01 d0                   add    %edx,%eax
  56:   3d 18 fc ff ff          cmp    $0xfffffc18,%eax # compare to -1000
  5b:   7d 07                   jge    64 <_Z22IsSumInRangeWithoutVarii+0x2f>
  5d:   b8 00 00 00 00          mov    $0x0,%eax
  62:   eb 05                   jmp    69 <_Z22IsSumInRangeWithoutVarii+0x34>
  64:   b8 01 00 00 00          mov    $0x1,%eax
  69:   5d                      pop    %rbp
  6a:   c3                      retq

000000000000006b <_Z26IsSumInRangeSuperOptimizedii>:
  6b:   55                      push   %rbp
  6c:   48 89 e5                mov    %rsp,%rbp
  6f:   89 7d fc                mov    %edi,-0x4(%rbp)
  72:   89 75 f8                mov    %esi,-0x8(%rbp)
  75:   8b 55 fc                mov    -0x4(%rbp),%edx
  78:   8b 45 f8                mov    -0x8(%rbp),%eax
  7b:   01 d0                   add    %edx,%eax
  7d:   3d 18 fc ff ff          cmp    $0xfffffc18,%eax
  82:   7c 16                   jl     9a <_Z26IsSumInRangeSuperOptimizedii+0x2f>
  84:   8b 55 fc                mov    -0x4(%rbp),%edx
  87:   8b 45 f8                mov    -0x8(%rbp),%eax
  8a:   01 d0                   add    %edx,%eax
  8c:   3d e8 03 00 00          cmp    $0x3e8,%eax
  91:   7f 07                   jg     9a <_Z26IsSumInRangeSuperOptimizedii+0x2f>
  93:   b8 01 00 00 00          mov    $0x1,%eax
  98:   eb 05                   jmp    9f <_Z26IsSumInRangeSuperOptimizedii+0x34>
  9a:   b8 00 00 00 00          mov    $0x0,%eax
  9f:   5d                      pop    %rbp
  a0:   c3                      retq

Biz yığını adreslerden görebilirsiniz (örneğin, -0x4içinde mov %edi,-0x4(%rbp)karşı -0x14in mov %edi,-0x14(%rbp)) IsSumInRangeWithVar()yığın 16 ekstra bayt kullanır.

Çünkü IsSumInRangeWithoutVar()ayırdığı yığında boşluk ara değer kaydetmek için sbu 2 talimatlar uzun olan bu uygulama ile sonuçlanan, yeniden hesaplamak gerekir.

Komik, ilk önce -1000 ve 1000 saniye ile karşılaştırmak dışında IsSumInRangeSuperOptimized()çok benziyor IsSumInRangeWithoutVar().

Şimdi sadece en temel optimizasyonlarla derlemek bakalım: g++ -O1 -c -o test.o test.cpp. Sonuç:

0000000000000000 <_Z19IsSumInRangeWithVarii>:
   0:   8d 84 37 e8 03 00 00    lea    0x3e8(%rdi,%rsi,1),%eax
   7:   3d d0 07 00 00          cmp    $0x7d0,%eax
   c:   0f 96 c0                setbe  %al
   f:   c3                      retq

0000000000000010 <_Z22IsSumInRangeWithoutVarii>:
  10:   8d 84 37 e8 03 00 00    lea    0x3e8(%rdi,%rsi,1),%eax
  17:   3d d0 07 00 00          cmp    $0x7d0,%eax
  1c:   0f 96 c0                setbe  %al
  1f:   c3                      retq

0000000000000020 <_Z26IsSumInRangeSuperOptimizedii>:
  20:   8d 84 37 e8 03 00 00    lea    0x3e8(%rdi,%rsi,1),%eax
  27:   3d d0 07 00 00          cmp    $0x7d0,%eax
  2c:   0f 96 c0                setbe  %al
  2f:   c3                      retq

Şuna bakar mısın: Her değişken aynıdır . Derleyici oldukça akıllıca bir şey yapabilir: abs(a + b) <= 1000eşdeğerdir a + b + 1000 <= 2000dikkate setbeyüzden negatif bir sayı çok büyük pozitif sayı olur imzalanmamış karşılaştırma gelmez. leaTalimat aslında bir talimat bütün bu eklemeler yapmak ve tüm koşullu dallarını ortadan kaldırabilir.

Sorunuzu yanıtlamak için, neredeyse her zaman için en uygun hale getirmek için gereken şey hafıza veya hız değil, okunabilirliktir . Kod okumak, yazmaktan daha zordur ve "optimize etmek" için ayarlanan kod okumak, açık olması için yazılmış kodlardan çok daha zordur. Çoğu zaman, bu "optimizasyonların" ihmal edilebilir olması veya bu durumda performans üzerinde tam olarak sıfır bir etki yaratması gibi.


Takip eden soru, bu kod derlenmiş yerine yorumlanmış bir dilde olduğunda ne değişir? Öyleyse, optimizasyon önemli mi yoksa aynı sonucu veriyor mu?

Ölçelim! Örnekleri Python'a kopyaladım:

def IsSumInRangeWithVar(a, b):
    s = a + b
    if s > 1000 or s < -1000:
        return False
    else:
        return True

def IsSumInRangeWithoutVar(a, b):
    if a + b > 1000 or a + b < -1000:
        return False
    else:
        return True

def IsSumInRangeSuperOptimized(a, b):
    return abs(a + b) <= 1000

from dis import dis
print('IsSumInRangeWithVar')
dis(IsSumInRangeWithVar)

print('\nIsSumInRangeWithoutVar')
dis(IsSumInRangeWithoutVar)

print('\nIsSumInRangeSuperOptimized')
dis(IsSumInRangeSuperOptimized)

print('\nBenchmarking')
import timeit
print('IsSumInRangeWithVar: %fs' % (min(timeit.repeat(lambda: IsSumInRangeWithVar(42, 42), repeat=50, number=100000)),))
print('IsSumInRangeWithoutVar: %fs' % (min(timeit.repeat(lambda: IsSumInRangeWithoutVar(42, 42), repeat=50, number=100000)),))
print('IsSumInRangeSuperOptimized: %fs' % (min(timeit.repeat(lambda: IsSumInRangeSuperOptimized(42, 42), repeat=50, number=100000)),))

Python 3.5.2 ile çalıştırın, bu çıktıyı verir:

IsSumInRangeWithVar
  2           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 BINARY_ADD
              7 STORE_FAST               2 (s)

  3          10 LOAD_FAST                2 (s)
             13 LOAD_CONST               1 (1000)
             16 COMPARE_OP               4 (>)
             19 POP_JUMP_IF_TRUE        34
             22 LOAD_FAST                2 (s)
             25 LOAD_CONST               4 (-1000)
             28 COMPARE_OP               0 (<)
             31 POP_JUMP_IF_FALSE       38

  4     >>   34 LOAD_CONST               2 (False)
             37 RETURN_VALUE

  6     >>   38 LOAD_CONST               3 (True)
             41 RETURN_VALUE
             42 LOAD_CONST               0 (None)
             45 RETURN_VALUE

IsSumInRangeWithoutVar
  9           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 BINARY_ADD
              7 LOAD_CONST               1 (1000)
             10 COMPARE_OP               4 (>)
             13 POP_JUMP_IF_TRUE        32
             16 LOAD_FAST                0 (a)
             19 LOAD_FAST                1 (b)
             22 BINARY_ADD
             23 LOAD_CONST               4 (-1000)
             26 COMPARE_OP               0 (<)
             29 POP_JUMP_IF_FALSE       36

 10     >>   32 LOAD_CONST               2 (False)
             35 RETURN_VALUE

 12     >>   36 LOAD_CONST               3 (True)
             39 RETURN_VALUE
             40 LOAD_CONST               0 (None)
             43 RETURN_VALUE

IsSumInRangeSuperOptimized
 15           0 LOAD_GLOBAL              0 (abs)
              3 LOAD_FAST                0 (a)
              6 LOAD_FAST                1 (b)
              9 BINARY_ADD
             10 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             13 LOAD_CONST               1 (1000)
             16 COMPARE_OP               1 (<=)
             19 RETURN_VALUE

Benchmarking
IsSumInRangeWithVar: 0.019361s
IsSumInRangeWithoutVar: 0.020917s
IsSumInRangeSuperOptimized: 0.020171s

Python'daki demontaj çok ilginç değil çünkü bytecode "compiler" optimizasyonu için pek bir şey yapmıyor.

Üç fonksiyonun performansı neredeyse aynı. IsSumInRangeWithVar()Marjinal hız artışı nedeniyle birlikte gitmeye cazip gelebiliriz . Her ne kadar farklı parametreler denediğimi eklesem de timeit, bazen IsSumInRangeSuperOptimized()en hızlı şekilde çıktığım için, herhangi bir uygulamanın kendine özgü avantajlarından ziyade farklılıktan sorumlu dış faktörler olabileceğinden şüpheleniyorum.

Bu gerçekten performans açısından kritik bir kodsa, yorumlanmış bir dil sadece çok kötü bir seçimdir. Aynı programı pypy ile çalıştırarak alıyorum:

IsSumInRangeWithVar: 0.000180s
IsSumInRangeWithoutVar: 0.001175s
IsSumInRangeSuperOptimized: 0.001306s

Sadece tercüman ek yükünü ortadan kaldırmak için JIT derlemesi kullanan pypy kullanmak, 1 veya 2 büyüklük sırasına göre performans artışı sağladı. IsSumInRangeWithVar()Diğerlerinden daha hızlı bir büyüklük sırası olduğunu görmek beni oldukça şaşırttı . Ben de ölçütlerin sırasını değiştirdim ve tekrar koştum:

IsSumInRangeSuperOptimized: 0.000191s
IsSumInRangeWithoutVar: 0.001174s
IsSumInRangeWithVar: 0.001265s

Öyle görünüyor ki, bunu hızlı yapan uygulama ile ilgili hiçbir şey değil, kıyaslama yaptığım sırayla!

Buna daha derinden dalmayı çok isterim, çünkü dürüst olmak gerekirse, bunun neden olduğunu bilmiyorum. Ancak konunun yapıldığına inanıyorum: ara değerin değişken olarak bildirilip bildirilmemesi gibi mikro optimizasyonlar nadiren alakalı değildir. Tercüme edilmiş bir dil veya yüksek derecede optimize edilmiş bir derleyici ile, ilk amaç hala net kod yazmaktır.

Ayrıca optimizasyon gerekebilir Eğer kriter . En iyi optimizasyonların küçük ayrıntılardan değil, daha büyük algoritmik görüntüden geldiğini unutmayın: pypy aynı fonksiyonun tekrar tekrar değerlendirilmesi için cpython'dan daha hızlı bir büyüklük sırası olacak çünkü cpython'dan daha hızlı algoritmalar kullanır (JIT derleyici - yorumlama) programı. Ayrıca dikkate alınması gereken kodlanmış bir algoritma var: B-ağacı üzerinden yapılan bir arama, bağlantılı bir listeden daha hızlı olacaktır.

İş için doğru araçları ve algoritmaları kullandığınızdan emin olduktan sonra , sistemin ayrıntılarına derinlemesine dalmaya hazır olun . Sonuçlar, deneyimli geliştiriciler için bile çok şaşırtıcı olabilir ve bu yüzden değişiklikleri ölçmek için bir kritere sahip olmalısınız.


6
C bir örnek sağlamak için: SharpLab Her iki yöntem için de aynıdır asm üretir (x 86 Masaüstü CLR v4.7.3130.00 (clr.dll))
VisualMelon

2
@VisualMelon, pozitif kontrol yeterince eğlenceli: "return (((a + b)> = -1000) && ((a + b) <= 1000));" farklı bir sonuç verir. : sharplab.io/…
Pieter B

12
Okunabilirlik, bir programı da optimize etmeyi kolaylaştırabilir. Derleyici yukarıda olduğu gibi eşdeğer bir mantığı kullanmak için kolayca yeniden yazabilir , ancak aslında ne yapmaya çalıştığınızı anlayabilir. Çok sayıda eski okul bithack kullanırsanız , inç ve işaretçiler arasında ileri geri yayın yaparsanız , değişken depolamayı tekrar kullanırsınız, vb. , yetersiz olabilir.
Leushenko

1
@Corey düzenleme bakın.
Phil Frost

2
@Corey: bu cevap aslında size tam olarak cevabımda ne yazdığımı söylüyor: düzgün bir derleyici kullanırken hiçbir fark yoktur ve bunun yerine hazırlığa odaklanın. Tabii ki, daha iyi kurulmuş görünüyor - belki şimdi bana inanıyorsunuz.
Doktor Brown

67

Belirtilen soruyu cevaplamak için:

Bir yöntem için bellek vs performans hızı ne zaman optimize etmek?

Yapman gereken iki şey var:

  • Başvurunuzu sınırlandırmak nedir?
  • Bu kaynağın çoğunu nereden geri alabilirim?

İlk soruyu cevaplamak için, uygulamanız için performans gereksinimlerinin ne olduğunu bilmek zorundasınız. Performans gereksinimi yoksa, bir veya diğerini optimize etmek için hiçbir sebep yoktur. Performans gereklilikleri "yeterince iyi" yerine ulaşmanıza yardımcı olur.

Kendi başına verdiğiniz yöntem, kendi başına herhangi bir performans sorununa neden olmaz, ancak belki de bir döngü ve büyük miktarda veri işlerken, soruna nasıl yaklaştığınız hakkında biraz farklı düşünmeye başlamanız gerekir.

Uygulamayı neyin sınırlandırdığını tespit etmek

Uygulamanızın davranışına bir performans izleyicisi ile bakmaya başlayın. Çalışırken CPU, disk, ağ ve bellek kullanımına bir göz atın. Her şey orta derecede kullanılırken bir veya daha fazla ürün en üst düzeye çıkarılır - eğer mükemmel dengeye gelmezseniz (ama bu asla gerçekleşmez).

Daha derin bakmanız gerektiğinde, genellikle bir profiler kullanırsınız . Orada bellek profilleyicilerini ve süreç profilleyicilerini ve bunlar farklı şeyler ölçerler. Profil hareketinin önemli bir performans etkisi var, ancak neyin yanlış olduğunu bulmak için kodunuzu kullanıyorsunuz.

CPU ve disk kullanımınızı zirveye çıkardığınızı varsayalım. Önce "sıcak noktalar" veya diğerlerinden daha sık olarak adlandırılan veya işlemin önemli bir yüzdesini alan kodu kontrol edersiniz.

Sıcak noktalar bulamazsanız, hafızaya bakmaya başlarsınız. Belki de gereğinden fazla nesne yaratıyorsunuz ve çöp koleksiyonunuz fazla mesai yapıyor.

Geri kazanma performansı

Eleştirel düşün. Aşağıdaki değişiklik listesi, ne kadar yatırım getirisi alacağınıza göredir:

  • Mimarlık: iletişimde boğulma noktaları arayın
  • Algoritma: Verileri işleme şeklinizin değişmesi gerekebilir
  • Sıcak noktalar: sıcak bölgeyi ne sıklıkta çağırırsanız küçültmek büyük bir avantaj sağlayabilir.
  • Mikro optimizasyonlar: Yaygın değildir, ancak bazen küçük kodları düşünmeniz gerekir (verdiğiniz örnek gibi), özellikle de kodunuzda sıcak bir nokta varsa.

Bu gibi durumlarda, bilimsel yöntemi uygulamanız gerekir. Bir hipotezle gelin, değişiklikleri yapın ve test edin. Performans hedeflerinize ulaşırsanız işiniz biter. Değilse, listedeki bir sonraki şeye gidin.


Soruyu kalın harflerle cevaplama:

Metot A ile Metot B'nin ne zaman ve ne zaman kullanılması uygundur?

Dürüst olmak gerekirse, bu performans veya hafıza problemleriyle uğraşmada son adımdır. Yöntem A ile Yöntem B'nin etkisi, dile ve platforma bağlı olarak (bazı durumlarda) gerçekten farklı olacaktır .

Hemen hemen yarıda iyi bir optimize ediciye sahip derlenmiş herhangi bir dil bu yapılardan herhangi biriyle benzer bir kod üretecektir. Bununla birlikte, bu varsayımların, optimize edicisi olmayan özel ve oyuncak dillerinde mutlaka geçerli olduğu söylenemez.

Tam olarak hangisinin daha iyi bir etkiye sahip olacağı sumbir yığın değişkeni veya bir yığın değişkeni olmasına bağlıdır . Bu bir dil uygulamasıdır. Örneğin, C, C ++ ve Java'da, bir gibi ilkel sayılar int, varsayılan olarak yığın değişkenleridir. Kodunuz, tamamen satır içi kodla sahip olacağınızdan daha büyük bir yığın değişkenine atama yaparak daha fazla bellek etkisine sahip değildir.

C kitaplıklarında (özellikle eski kitaplarda) bulabileceğiniz diğer optimizasyonlar, önce veya ilk önce 2 boyutlu bir diziyi kopyalamak arasında karar vermek zorunda kalabileceğiniz bir platforma bağlı optimizasyondur. Hedeflediğiniz yonga setinin hafıza erişimini en iyi şekilde nasıl optimize ettiği konusunda biraz bilgi sahibi olmanız gerekiyor. Mimariler arasında ince farklılıklar var.

Alt satırda optimizasyon sanat ve bilimin bir birleşimidir. Bazı eleştirel düşünme ve soruna nasıl yaklaştığınız konusunda bir derece esneklik gerektirir. Küçük şeyleri suçlamadan önce büyük şeyleri arayın.


2
Bu cevap en çok sorumu ele alıyor ve kodlama örnekleriime takılmıyor, yani Yöntem A ve Yöntem B
Corey P

18
Bunun "performans darboğazlarını nasıl ele alıyorsunuz?" Un genel cevabı olduğunu hissediyorum, ancak bu yöntemi kullanarak 4 veya 5 değişkene sahip olup olmadığına bağlı olarak belirli bir işlevden göreceli bellek kullanımını tanımlamanız zor olacaktır. Ayrıca, derleyicinin (veya tercümanın) bu durumu en iyi duruma getirip getiremeyeceği veya veremeyeceği durumlarda, bu optimizasyon seviyesinin ne kadar alakalı olduğunu da sorguluyorum.
Eric

@Eric, daha önce de belirttiğim gibi, performans gelişimi için son kategori, mikro optimizasyonlarınız olacaktır. Herhangi bir etkisi olacaksa iyi bir tahminde bulunmanın tek yolu, bir profildeki performansı / belleği ölçmektir. Bu tür iyileştirmelerin getirisi nadirdir, ancak zaman zaman hassas performans problemlerinde simülatörlerde iyi bir şekilde yerleştirilmiş bir çift değişimin, zamanlama hedefinize çarpmak ile zamanlama hedefinize ulaşmak arasındaki fark olabilir. Sanırım bir yandan 20 yıldan fazla bir süredir yazılım üzerinde çalışarak karşılığını verebileceğim kadar güvenebilirim, ama sıfır değil.
Berin Loritsch

@BerinLoritsch Yine, genel olarak sizinle aynı fikirdeyim, ancak bu özel durumda ben aynı fikirdeyim. Kendi cevabımı verdim, ancak kişisel olarak bir işlevin bellek boyutu ile ilgili performans sorunlarını tanımlamak için size yol gösterecek veya hatta size yol gösterecek herhangi bir araç görmedim.
Eric

@DocBrown, Bunu düzelttim. İkinci soruya gelince, sana çok katılıyorum.
Berin Loritsch

45

"Bu, belleği azaltacaktır" - em, hayır. Bu doğru olsa bile (ki bu iyi bir derleyici için geçerli değildir), fark gerçek dünyadaki herhangi bir durum için önemsizdir.

Ancak, A * yöntemini kullanmanızı tavsiye ederim (A yönteminde küçük bir değişiklik var):

private bool IsSumInRange(int a, int b)
{
    int sum = a + b;

    if (sum > 1000 || sum < -1000) return false;
    else return true;
    // (yes, the former statement could be cleaned up to
    // return abs(sum)<=1000;
    // but let's ignore this for a moment)
}

ancak iki tamamen farklı nedenlerden dolayı:

  • değişkene saçıklayıcı bir ad vererek, kod daha net hale gelir

  • Aynı toplama mantığına iki kez kodda sahip olmaktan kaçınır, böylece kod daha DRY olur, bu da değişikliklere daha az hataya neden olur.


36
Daha da temizler ve "return> -1000 && sum <1000;" ile giderdim.
26

36
@Corey herhangi bir terbiyeli optimize edici, sumdeğişken için bir CPU kaydı kullanacak ve böylece sıfır bellek kullanımına yol açacaktır . Ve olmasa bile, bu “yaprak” yönteminde yalnızca tek bir bellek kelimesidir. Ne kadar inanılmaz derecede boşa harcanan Java veya C # 'nın GC ve nesne modellerinden dolayı başka türlü olabileceği göz önüne alındığında, yerel bir intdeğişken kelimenin tam anlamıyla farkedilir bir hafıza kullanmaz. Bu anlamsız bir mikro optimizasyondur.
04:

10
@Corey: “ biraz daha karmaşık” ise, muhtemelen “farkedilir bir bellek kullanımı” haline gelmeyecektir. Belki daha karmaşık bir örnek oluşturursanız, ancak bu farklı bir soru yapar. Ayrıca, yalnızca bir ifade için belirli bir değişken oluşturmadığınız için, karmaşık ara sonuçlar için çalışma zamanı ortamı hala dahili olarak geçici nesneler oluşturabilir, bu nedenle tamamen dilin, ortamın, optimizasyon seviyesinin ve "farkedilebilir" dediğiniz şey.
Doktor Brown

8
Yukarıdaki noktalara ek olarak, C # / Java'nın depolamayı nasıl seçtiğinin sumbir uygulama detayı olacağından oldukça eminim ve bir kişinin yerelden kaçınmak gibi saçma bir hilenin intbuna yol açıp açmayacağı konusunda ikna edici bir dava açabileceğinden şüpheliyim. Uzun vadede bu miktarda bellek kullanımı. IMO okunabilirliği daha önemlidir. Okunabilirlik öznel olabilir, ancak FWIW, şahsen ben aynı hesaplamayı asla iki kez yapmamayı tercih ederim, CPU kullanımı için değil, ancak bir hata ararken yalnızca ekinizi bir kez kontrol etmek zorunda kalıyorum.
jrh

2
Genel (zaten C # için) sadece temizlenmiş olabilir tahmin edilemez, "bellek deniz çalkalama" o vardır içinde ... Ayrıca çöp toplanan dilleri dikkat gerektiğinde , ben RAM gigabayt tahsis bir program yapma hatırlar ve sadece başladı " Hafızanın azaldığı durumlarda "peşinden temizlik". GC'nin çalışması gerekmiyorsa, tatlı bir zaman alabilir ve daha acil konular için CPU'nuzu koruyabilir.
jrh

35

Her ikisinden de daha iyisini yapabilirsiniz.

return (abs(a + b) > 1000);

Çoğu işlemci (ve dolayısıyla derleyiciler) tek bir işlemde abs () yapabilir. Yalnızca daha az miktarda değil, aynı zamanda daha az karşılaştırmaya sahip olursunuz, bu da genellikle hesaplamalı olarak pahalıdır. Ayrıca, çoğu işlemcide daha kötü olan dallanmayı da ortadan kaldırır, çünkü boru hattının mümkün olmasını durdurur.

Diğer cevapların söylediği gibi görüşmeci, bitki yaşamıdır ve teknik görüşme yapacak bir işi yoktur.

Bu, onun sorusu geçerli olduğunu söyledi. Ve ne zaman optimize ettiğinize ve nasıl cevap vereceğinize cevabı, gerekli olduğunu kanıtladığınızda ve tam olarak hangi parçaların ihtiyaç duyduğunu kanıtlamak için profillendirdiniz . Knuth, erken optimizasyonun tüm kötülüklerin kökü olduğunu söyledi, çünkü gerçekten ihtiyaç duyulan yerleri kaçırırken, önemli olmayan bölümleri altın plakalarını denemek ya da hiçbir etkisi olmayan (görüşme yapan kişinin gibi) değişiklikler yapmak çok kolay. Elinizde kesin bir kanıt bulunana kadar gerçekten gerekli, kodun açıklığı en önemli hedef.

FabioTurati'yi düzenleme , bunun orijinalin zıddı mantık duygusudur (benim hatam!) Ve bunun, optimize etmeye çalışırken kodu kırma riskini taşıdığımız Knuth'un teklifinden başka bir etki gösterdiğini doğru bir şekilde belirtiyor.


2
@Corey, Graham'ın beklendiği gibi "aynı sorunu daha az değişkenle çözmeme zorladı " isteğini belirlediğinden eminim . Eğer görüşmeci olsaydım, bu cevabı beklerdim, a+biçine ifgirip iki kez yapmamayı . Yanlış anlamışsınız. "Bu, bu yöntemle bellek kullanımını azaltacağını söyledi ve memnuniyetini azalttı" dedi. Burada soru sormayı ciddiye almamalısın. Bir iş aldın mı? Sanırım yapmadın :-(
Sinatr

1
Aynı anda 2 dönüşüm uyguluyorsunuz: kullanarak 2 koşulu 1'e çevirdiniz abs(), ayrıca returnkoşul doğru olduğunda bir tanesine ("dal" ise) ve yanlış olduğunda başka bir taneye sahip olmalısınız. "else dalı"). Bu gibi bir kodu değiştirdiğinizde, dikkatli olun: yanlışlıkla yanlış döndüğünde doğru olan bir işlevi yanlışlıkla yazma ya da tam tersi durumda olan bir fonksiyon yazma riski vardır. Burada tam olarak ne oldu. Başka bir şeye odaklandığını ve iyi bir iş çıkardığını biliyorum. Yine de, bu size işinize kolayca mal olabilirdi ...
Fabio Turati

2
@FabioTurati Peki benekli - teşekkürler! Cevabı güncelleyeceğim. Ve bu yeniden yapılandırma ve optimizasyon için iyi bir nokta, bu Knuth'un teklifini daha da alakalı hale getiriyor. Riske girmeden önce optimizasyona ihtiyacımız olduğunu kanıtlamalıyız.
Graham,

2
Çoğu işlemci (ve dolayısıyla derleyiciler) tek bir işlemde abs () yapabilir. Tamsayılar için ne yazık ki durum böyle değil. ARM64 o bayraklar zaten gelen ayarlanmış olup olmadığını kullanabileceğiniz bir koşullu olumsuzlamak sahiptir addsve ARM ters sub esas olan ( rsblt= ters-sub az-tha ise) ama her şey uygulamak için birden fazla talimat gerektirir abs(a+b)veya abs(a). godbolt.org/z/Ok_Con , x86, ARM, AArch64, PowerPC, MIPS ve RISC-V asm çıktısını gösterir. Sadece karşılaştırmayı bir poligon kontrolüne dönüştürerek (unsigned)(a+b+999) <= 1998U, gcc'nin Phil'in cevabında olduğu gibi onu optimize edebileceğidir.
Peter Cordes

2
Bu cevaptaki "gelişmiş" kod farklı bir cevap ürettiğinden yanlıştır IsSumInRange(INT_MIN, 0). Orijinal kod falseçünkü döner INT_MIN+0 > 1000 || INT_MIN+0 < -1000; ancak "yeni ve geliştirilmiş" kod trueçünkü döner abs(INT_MIN+0) < 1000. (Veya bazı dillerde, bir istisna atar veya tanımsız bir davranışı olur. Yerel listelerinizi kontrol edin.)
Quuxplusone

16

Metot A ile Metot B'nin ne zaman ve ne zaman kullanılması uygundur?

Donanım ucuz; programcılar pahalıdır . Bu yüzden ikinize bu soruda harcadığınız zamanın maliyeti muhtemelen her iki cevaptan da çok daha kötü.

Ne olursa olsun, çoğu modern derleyici yerel değişkeni bir register'a (yığın alanı tahsis etmek yerine) optimize etmenin bir yolunu bulacaktır, bu yüzden metotlar çalıştırılabilir kod açısından muhtemelen aynıdır. Bu nedenle çoğu geliştirici, niyeti en açık şekilde bildiren seçeneği seçecektir (bkz. Gerçekten açık kod yazma (ROC) ). Bence bu A Yöntemi olurdu.

Öte yandan, eğer bu tamamen akademik bir egzersizse, Yöntem C ile her iki dünyanın da en iyisini elde edebilirsiniz:

private bool IsSumInRange(int a, int b)
{
    a += b;
    return (a >= -1000 && a <= 1000);
}

17
a+=bDüzgün bir numaradır, ancak şunu söylemeliyim (cevabın geri kalanından ima edilmediği takdirde), deneyimlerimde parametrelerle uğraşmak hata ayıklamak ve sürdürmek çok zor olabilir.
jrh

1
Ben katılıyorum. Ben ROC için güçlü bir savunucuyum ve bu tür bir şey dışında.
John Wu

3
"Donanım ucuz; programcılar pahalı." Tüketici elektroniği dünyasında bu ifade yanlıştır. Milyonlarca birim satıyorsanız, birim başına donanım maliyetlerinde 0,10 ABD Doları kazanmak için ek geliştirme maliyetinde 500.000 ABD Doları harcamak çok iyi bir yatırımdır.
Bart van Ingen Schenau

2
@JohnWu: Hesabı basitleştirdin if, ancak karşılaştırmanın sonucunu tersine çevirmeyi unuttun; sizin işlevi artık dönüyor truezaman a + bolduğu değil aralığında. Koşulların !dışına bir ( return !(a > 1000 || a < -1000)) !ekleyin return a <= 1000 && a >= -1000;ya da aralık kontrolünün güzel bir şekilde akışını sağlamak için tersine çevirme testlerini return -1000 <= a && a <= 1000;
dağıtın

1
@JoWu: Sınır durumlarında hala biraz kapalı, dağıtılmış mantık <=/ gerektirir >=, </ >( </ / >, 1000 ve -1000 ile aralık dışı olarak kabul edilir, orijinal kod kendilerine aralık olarak kabul edilir).
ShadowRanger

11

Okunabilirlik için optimize ederdim. Yöntem X:

private bool IsSumInRange(int number1, int number2)
{
    return IsValueInRange(number1+number2, -1000, 1000);
}

private bool IsValueInRange(int Value, int Lowerbound, int Upperbound)
{
    return  (Value >= Lowerbound && Value <= Upperbound);
}

Sadece 1 şeyi yapan, ancak akla gelmesi kolay olan küçük yöntemler.

(Bu kişisel tercih, negatif yerine pozitif testlerden hoşlanıyorum, orijinal kodunuz aslında değerin aralık dışında olup olmadığını test ediyor.)


5
Bu. (Yukarıdaki benzer yorumların yeniden okunması benzerdi: okunabilirlik). 30 yıl önce, 1 MB'tan daha az RAM'e sahip makinelerle çalışırken, sıkma performansı gerekliydi - tıpkı y2k probleminde olduğu gibi, her birinin kullanılmamış varyasyonlar nedeniyle birkaç byte hafızasının boşa harcandığı birkaç yüz bin rekorunu kırdı ve referanslar, vs. Şimdi, çok gigabayt RAM'e sahip makinelerle uğraşıyoruz, birkaç MB RAM bile tasarruf ederek kod okunabilirliği ve kodun korunması iyi bir işlem değil.
ivanivan

@ivanivan: "y2k problemi" nin gerçekten hafıza ile ilgili olduğunu sanmıyorum. Veri girişi açısından, iki basamak girmek, dört girmekten daha etkilidir ve girişleri girilmiş olarak tutmak, onları başka bir forma dönüştürmekten daha kolaydır.
supercat,

10
Şimdi ne olduğunu görmek için 2 işlevi izlemelisiniz. Gerçeğe uygun değerden alamazsınız, çünkü isimlerin bunların kapsayıcı mı yoksa özel sınırlar mı olduğunu söyleyemezsiniz. Ve bu bilgiyi eklerseniz, işlevin adı onu ifade eden koddan daha uzundur.
Peter,

1
Okunabilirliği en iyi duruma getirin ve küçük, kolay anlaşılır işlevler yapın - kesin, katılıyorum. Ama şiddetle bu adlandırma katılmıyorum ave bhiç number1ve number2hiçbir şekilde yardımcı okunabilirliği. Ayrıca, fonksiyonların isimlendirilmesi de tutarsızdır: argüman olarak kabul IsSumInRangeederse neden aralığı zorlaştırıyor IsValueInRange?
leftaroundabout

1. fonksiyon taşabilir. (Diğer cevapların kodu gibi.) Taşma emniyetli kodun karmaşıklığı bir fonksiyona koymak için bir argüman olmasına rağmen.
philipxy

6

Kısacası, sorunun şu anki bilgisayarla ilgisi olduğunu sanmıyorum, ancak tarihsel açıdan ilginç bir düşünce alıştırması.

Mülakatınız muhtemelen Efsanevi Adam Ayının hayranı. Kitapta Fred Brooks, programcıların araç kutularında genellikle iki temel fonksiyon sürümüne ihtiyaç duyacaklarını ortaya koyuyor: bellek için optimize edilmiş bir versiyon ve cpu için optimize edilmiş bir versiyon. Fred bunu, makinelerin 8 kilobayt RAM'e kadar sahip olabileceği IBM System / 360 işletim sisteminin geliştirilmesine öncülük etti. Bu tür makinelerde, işlevlerdeki yerel değişkenler için gereken bellek, özellikle de derleyici bunları etkin bir şekilde optimize etmediyse (ya da kod doğrudan assembly dilinde yazılmışsa) potansiyel olarak önemli olabilir.

Şu anki dönemde, bir yöntemde yerel bir değişkenin varlığının veya yokluğunun gözle görülür bir fark yaratacağı bir sistem bulmakta zorlanacağınızı düşünüyorum. Bir değişkenin önemli olması için, yöntemin derin özyinelemenin beklendiği gibi özyinelemeli olması gerekir. O zaman bile, değişkenin kendisi bir soruna neden olmadan önce yığın derinliğinin aşılması muhtemeldir, Stack Overflow istisnalarına neden olur. Bir sorun olabileceği tek gerçek senaryo, özyinelemeli bir yöntemle yığında tahsis edilen dizilerin çok büyük olmasıdır. Ancak, çoğu geliştiricinin, büyük dizilerin gereksiz kopyalarını iki kez düşüneceğini düşündüğümden de, bu mümkün değildir.


4

Atamadan sonra s = a + b; a ve b değişkenleri artık kullanılmamaktadır. Bu nedenle, tamamen beyin hasarlı bir derleyici kullanmıyorsanız, s için hiçbir bellek kullanılmaz; Zaten a ve b için kullanılan hafıza yeniden kullanılıyor.

Ancak bu işlevi optimize etmek tamamen saçma. Yerden tasarruf ederseniz, işlev çalışırken (işlev döndüğünde kurtarılan) işlev 8 saniye olabilir, yani kesinlikle anlamsız. Eğer zaman kazanabilseydiniz, tek sayıdaki nanosaniye olurdu. Bunu optimize etmek tam bir zaman kaybı.


3

Yerel değer türü değişkenleri yığın üzerinde tahsis edilir veya (bu tür küçük kod parçaları için daha muhtemeldir) işlemcideki kayıtları kullanır ve hiçbir RAM göremez. Her iki durumda da kısa ömürlü ve endişelenecek bir şey yok. Potansiyel olarak büyük ve uzun ömürlü olan koleksiyonlardaki veri öğelerini tamponlamanız veya sıraya koymanız gerektiğinde bellek kullanımını göz önünde bulundurmaya başlarsınız.

O zaman uygulamanız için en çok neye değer verdiğinize bağlı. İşleme hızı? Tepki Süresi? Hafıza ayak izi? İdame? Tasarımda tutarlılık? Her şey sana bağlı.


4
Nitpicking: En azından .NET (yazının dili belirtilmemiş), yerel değişkenlerin "yığında" tahsis edildiğine dair hiçbir garanti vermez. Bkz . Eric Lippert "Yığın Bir Uygulama Detayı".
jrh

1
@jrh Yığın veya yığıntaki yerel değişkenler bir uygulama detayı olabilir, ancak birisi yığında gerçekten bir değişken isterse stackallocşimdi ve orada Span<T>. Profillemeden sonra sıcak noktalarda kullanışlıdır. Ayrıca, yapıların etrafındaki belgelerin bazıları, referans türleri olmayacakken değer türlerinin yığın üzerinde olabileceği anlamına gelir . Neyse, en iyi ihtimalle biraz GC'den kaçınabilirsiniz.
Bob

2

Diğer cevapların söylediği gibi, ne için optimize ettiğinizi düşünmeniz gerekir.

Bu örnekte, düzgün bir derleyicinin her iki yöntem için de eşdeğer kod üreteceğinden şüpheliyim, bu nedenle kararın çalışma süresi veya bellek üzerinde bir etkisi olmaz !

Ne gelmez etkileyen kod okunabilirliği olduğunu. (Kod, insanların okuması içindir, yalnızca bilgisayarları değil.) İki örnek arasında çok fazla fark yoktur; Tüm diğer şeyler eşit olduğunda, kısalık bir erdem olarak kabul edilir, bu yüzden muhtemelen Yöntem B'yi seçerdim. Fakat diğer tüm şeyler nadiren eşittir ve daha karmaşık bir gerçek dünya durumunda, bunun büyük bir etkisi olabilir.

Düşünülmesi gereken şeyler:

  • Ara ifadenin herhangi bir yan etkisi var mı? Herhangi bir saf olmayan işlev çağırırsa veya herhangi bir değişkeni güncellerse, elbette çoğaltmak sadece stil değil, bir doğruluk meselesi olacaktır.
  • Ara ifade ne kadar karmaşık? Çok fazla hesaplama yaparsa ve / veya çağırırsa, derleyici bunu optimize edemeyebilir ve bu da performansı etkileyebilir. (Knuth'un dediği gibi , “Küçük verimleri unutmalıyız, zamanın yaklaşık% 97'sini söyleyelim”).
  • Ara değişkenin bir anlamı var mı? Neler olduğunu açıklamaya yardımcı olacak bir isim verilebilir mi? Kısa ama bilgilendirici bir isim kodu daha iyi açıklayabilirken, anlamsız olanı sadece görsel gürültüdir.
  • Ara ifade ne kadar sürüyor? Uzun sürerse, çoğaltılması kodu daha uzun ve okumayı zorlaştırabilir (özellikle satır sonu zorlarsa); değilse, çoğaltma her şeyden daha kısa olabilir.

1

Yanıtların çoğunun işaret ettiği gibi, bu işlevi modern derleyicilerle ayarlamaya çalışmak bir fark yaratmaz. Bir optimizer büyük olasılıkla en iyi çözümü bulabilir (bunu kanıtlamak için assembler kodunu gösteren cevaba oylama!). Röportajdaki kodun tam olarak karşılaştırmanız istenen kod olmadığını söylediniz, bu yüzden belki de gerçek örnek biraz daha mantıklı geliyor.

Fakat şu soruya bir daha bakalım: Bu bir röportaj sorusu. Yani asıl mesele, denemek ve işi almak istediğinizi varsayarak nasıl cevap vermelisiniz?

Ayrıca görüşmeci'nin neden bahsettiğini bildiğini ve sadece ne bildiğini görmeye çalıştığını varsayalım.

İyileştiriciyi yok sayarak, ilk yığında geçici bir değişken oluşturabilirken ikincisinin yapamayacağını ancak hesaplamayı iki kez gerçekleştireceğini söyleyeceğim. Bu nedenle, ilk önce daha fazla bellek kullanır ancak daha hızlıdır.

Yine de, bir hesaplamanın sonucu saklamak için geçici bir değişken gerektirebileceğini (bunun karşılaştırılması için), bu değişkeni isimlendirmek veya bir fark yaratmamak gerekebileceğini belirtebilirsiniz.

Daha sonra, gerçekte kodun optimize edileceğini ve tüm değişkenlerin yerel olduğu için büyük olasılıkla eşdeğer makine kodunun üretileceğini söyleyeceğim. Ancak, hangi derleyiciyi kullandığınıza bağlı (yerel bir değişkeni Java'da "final" olarak ilan ederek faydalı bir performans iyileştirme elde etmem uzun zaman önceydi).

Herhangi bir durumda yığının kendi bellek sayfasında yaşadığını belirtebilirsiniz, bu nedenle ekstra değişkeniniz yığının sayfadan taşmasına neden olmadıkça, gerçekte daha fazla bellek ayırmaz. Taşması durumunda tamamen yeni bir sayfa isteyecektir.

Daha gerçekçi bir örneğin, birçok hesaplamanın sonucunu elde etmek için bir önbellek kullanıp kullanmama seçimi olabileceğinden ve bunun cpu ile hafıza sorununu gündeme getireceğinden bahsedeceğim.

Bütün bunlar ne hakkında konuştuğunu bildiğini gösteriyor.

Bunun yerine okunabilirliğe odaklanmanın daha iyi olacağını söylemeye son veririm. Bu durumda doğru olsa da, görüşme bağlamında "Performans hakkında bilgim yok ama kodum Janet ve John hikayesi gibi okuyor " olarak yorumlanabilir.

Yapmamanız gereken, kod optimizasyonunun nasıl gerekli olmadığına dair olağan mülayim ifadelerini ortadan kaldırmak, kodu belirleyene kadar optimize etmeyin (bu sadece kendiniz için kötü kod göremediğinizi gösterir), donanım maliyetleri programcılardan daha düşüktür ve lütfen, lütfen, lütfen Knuth "erken falan filan ..." deme.

Kod performansı birçok organizasyonda gerçek bir konudur ve birçok kuruluşun bunu anlayan programcılara ihtiyacı vardır.

Özellikle, Amazon gibi kuruluşlarla birlikte, kodun bir kısmının çok büyük kaldıraçları vardır. Bir kod pasajı, binlerce sunucuya veya milyonlarca cihaza dağıtılabilir ve yılın her günü milyarlarca kez çağrılabilir. Binlerce benzer snippet olabilir. Kötü bir algoritma ve iyi bir algoritma arasındaki fark kolayca bin faktör olabilir. Sayıları yapın ve hepsini bu kadar yapın: fark yaratır. Bir sistemin kapasite yetersiz kalması durumunda, performans göstermeyen kodun düzenlenmesi için potansiyel maliyet çok önemli veya ölümcül olabilir.

Dahası, bu kuruluşların çoğu rekabetçi bir ortamda çalışıyor. Bu nedenle, rakiplerinizin yazılımı zaten sahip oldukları donanımda iyi çalışıyorsa veya yazılım bir mobil telefonda çalışıyorsa ve yükseltilemiyorsa, müşterilerinize daha büyük bir bilgisayar almalarını söyleyemezsiniz. Bazı uygulamalar özellikle performans açısından kritik öneme sahiptir (oyunlar ve mobil uygulamalar akla gelir) ve duyarlılıklarına veya hızlarına göre yaşayabilir veya ölebilir.

Kişisel olarak yirmi yıldan uzun bir süredir performans sorunları nedeniyle sistemlerin başarısız olduğu veya kullanılamadığı birçok projede çalıştım ve bu sistemleri optimize etmek için çağrıldım ve her durumda, anlamayan programcılar tarafından yazılan kötü kodlar nedeniyle oldu. yazdıkları şeyin etkisi. Dahası, asla bir kod parçası değildir, her zaman her yerdedir. Açtığımda, performansı düşünmeye başlamanın geç bir yolu var: Hasar gerçekleşti.

Kod performansını anlamak, kod doğruluğunu ve kod stilini anlamada olduğu gibi iyi bir beceridir. Uygulamadan çıkar. Performans başarısızlıkları, fonksiyonel başarısızlıklar kadar kötü olabilir. Sistem çalışmıyorsa, çalışmaz. Neden olduğu önemli değil. Benzer şekilde, hiç kullanılmayan performans ve özelliklerin ikisi de kötüdür.

Bu nedenle, görüşmeci size performans hakkında sorular sorarsa, mümkün olduğunca fazla bilgi edinmeyi denemeyi tavsiye ederim. Eğer soru kötü görünüyorsa, niçin bu durumda bir sorun olmayacağını düşündüğünüzü açıkça belirtiniz. Knuth'tan alıntı yapma.


0

Önce doğruluk için optimizasyon yapmalısınız.

İşleviniz, Int.MaxValue değerine yakın giriş değerleri için başarısız olur:

int a = int.MaxValue - 200;
int b = int.MaxValue - 200;
bool inRange = test.IsSumInRangeA(a, b);

Bu doğru olur çünkü toplam -400'e taşar. İşlev ayrıca bir = int.MinValue + 200 için çalışmaz. (Yanlış "400" e kadar ekler)

Görüşme yapan kişinin çan sesi çıkarmadan ne aradığını bilemeyiz, ancak “taşma gerçektir” .

Görüşme durumunda, sorunun kapsamını netleştirmek için sorular sorun: İzin verilen maksimum ve minimum giriş değerleri nedir? Bunlara sahip olduğunuzda, arayan kişi aralığın dışındaki değerleri girerse bir istisna atabilirsiniz. Veya (C # 'da), taşma konusunda bir istisna atacak olan işaretlenmiş bir {} bölümünü kullanabilirsiniz. Evet, daha fazla iş ve karmaşık, ama bazen gereken bu.


Yöntemler sadece örnekti. Doğru olmak için yazılmamışlar, ancak asıl soruyu göstermek için yazılmıştır. Yine de giriş için teşekkürler!
Corey P

Bence görüşme sorusu performansa yönelik, bu nedenle sorunun amacını cevaplamanız gerekiyor. Görüşme yapan kişi sınırdaki davranış hakkında soru sormuyor. Ama yine de ilginç yan nokta.
rghome

1
@Corey İyi görüşmeciler soru olarak 1) konuyla ilgili aday kabiliyetini, burada yine de rghome tarafından önerildiği gibi değerlendirir; daha sonra kariyer görüşmelerinde - iyi şanslar.
chux

0

Sorunuz şu olmalıydı: "Bunu optimize etmek zorunda mıyım?".

A ve B sürümleri, A'yı tercih edilebilir kılan önemli bir ayrıntıda farklıdır, ancak optimizasyonla ilgisi yoktur: Kodu tekrarlamıyorsunuz.

Gerçek "optimizasyon", her derleyicinin hemen hemen yaptığı şey olan ortak alt ifadenin kaldırılması olarak adlandırılır. Bazıları bu temel optimizasyonu, optimizasyonlar kapalı olsa bile yapar. Bu gerçekten bir optimizasyon değil (üretilen kod her durumda neredeyse tamamen aynı olacaktır).

Fakat bir optimizasyon değilse , neden tercih edilir? Pekala, kodu tekrar etmiyorsun, kimin umrunda!

Her şeyden önce, şartlı fıkranın yarısını yanlış anlama konusunda kaza riskiniz yoktur. Fakat daha da önemlisi, bu kodu okuyan bir kişi bir deneyim yerine hemen yapmaya çalıştığınız şeyi düzeltebilir if((((wtf||is||this||longexpression)))). Okuyucunun if(one || theother)gördüğü şey, ki bu iyi bir şey. Değil nadiren, o olur Eğer diğer kişinin üç yıl sonra kendi kodunuzu okuma ve düşünme o vardır "WTF bu demek oluyor?". Bu durumda, kodunuzun amacın ne olduğunu hemen bildirmesi her zaman yardımcı olacaktır. Ortak bir alt ifadenin uygun bir şekilde adlandırılmasıyla, durum budur.
Gelecekte herhangi bir zamanda, ihtiyacınız örn değiştirmeye karar verirseniz Ayrıca a+betmek a-b, değiştirmek zorunda biriniiki değil. Ve ikincisini kazayla yanlış yapma riski yoktur.

Asıl sorunuz hakkında, neye göre optimize etmeniz gerektiğini, öncelikle kodunuzun doğru olması gerekir . Bu kesinlikle en önemli şey. Doğru olmayan kod kötü koddur, hatta hatalı olmasına rağmen "iyi çalışıyor" veya en azından iyi çalışıyor gibi görünüyor olsa da moreso . Bundan sonra, kodun okunması gerekir (aşina olmayan biri tarafından okunabilir).
İyileştirme gelince ... kesinlikle bir kasıtlı olarak optimize edilmiş kod yazmamalı ve kesinlikle başlamadan önce tasarıma bir düşünce harcamaman gerektiğini söylemiyorum (örneğin, problem için doğru algoritmayı seçmek gibi) en az verimli olanı değil).

Ancak çoğu uygulama için, çoğu zaman, bir optimizasyon derleyici aracılığıyla makul bir algoritma kullanarak doğru, okunabilir kod çalıştırdıktan sonra elde ettiğiniz performans gayet iyidir, endişelenmenize gerek yoktur.

Durum böyle değilse, yani uygulamanın performansı gerçekten de gereklilikleri karşılamıyorsa ve ancak o zaman denediğiniz yerel optimizasyonları yapmaktan endişe etmelisiniz. Tercihen, yine de, üst seviye algoritmayı tekrar gözden geçirirsiniz. Daha iyi bir algoritma nedeniyle 50.000 yerine 500 kez bir işlev çağırırsanız, bunun mikro optimizasyonda üç saat döngüsünden tasarruf etmekten daha büyük bir etkisi olur. Eğer varsa yok rastgele bellek erişiminde birkaç yüz döngüleri için her zaman oyala, bu, vs vs bir kaç ucuz hesaplamalar ekstra yapmaktan daha büyük bir etkiye sahiptir

Optimizasyon zor bir meseledir (kitapların tamamını bu konuda yazabilir ve sonuna kadar varamazsınız), ve belirli bir noktayı (tümüyle tıkanıklık olup olmadığını bile bilmeden) kör bir şekilde optimize etmek için zaman harcamak genellikle zaman kaybıdır. Profil oluşturma olmadan optimizasyonun doğru yapılması çok zordur.

Ancak, bir kural olarak, kör uçtuğunuzda ve sadece bir şeyler yapmanız gerektiğinde / yapmak istediğinizde veya genel bir varsayılan strateji olarak, "bellek" için optimize etmenizi öneririm.
“Belleği” (özellikle mekansal konum ve erişim düzenleri) için optimize etmek genellikle bir fayda sağlar, çünkü bir zamanlar her şeyin “aynı” olduğu zamanların aksine, günümüzde RAM'e erişmek en pahalı şeyler arasındadır (diskten okuma yetersiz!). prensip olarak yapabilirsiniz. Öte yandan, ALU her hafta ucuz ve hızlı oluyor. Bellek bant genişliği ve gecikme süresi neredeyse hiç iyileşmiyor. İyi yerellik ve iyi erişim düzenleri, veri yoğun uygulamalardaki kötü erişim düzenleriyle karşılaştırıldığında çalışma zamanında kolayca 5 kat fark yaratabilir (aşırı, karmaşık örneklerde 20 kat). Önbelleklerine iyi davran, mutlu bir insan olacaksın.

Önceki paragrafı perspektife koymak için, yapabileceğiniz farklı şeylerin size neye mal olduğunu düşünün. Gibi bir a+bişlemi yapmak (optimize edilmemişse) bir veya iki devir alır, ancak CPU genellikle döngü başına birkaç talimat başlatabilir ve bağımlı olmayan komutları düzenleyebilir, böylece daha gerçekçi bir şekilde size sadece yaklaşık yarım devir veya daha kısa bir sürede mal olabilir. İdeal olarak, derleyici çizelgeleme işleminde iyiyse ve duruma bağlı olarak sıfıra mal olabilir.
Verileri ("bellek") almak, eğer şanslıysanız ve L1'de ise 4-5 döngüdür ve çok şanslı değilseniz (L2 hit) yaklaşık 15 döngüdür. Veri önbellekte hiç değilse, birkaç yüz döngü sürer. Tehlikeli erişim modeliniz TLB'nin yeteneklerini aşıyorsa (sadece ~ 50 girişle yapmak kolaydır), birkaç yüz döngü daha ekleyin. Tehlikeli erişim düzeniniz gerçekten bir sayfa hatasına neden oluyorsa, en iyi durumda birkaç on bin döngü ve en kötü durumda ise birkaç milyona mal olur.
Şimdi bir düşünün, en acilen kaçınmak istediğiniz şey nedir?


0

Bir yöntem için bellek vs performans hızı ne zaman optimize etmek?

Önce işlevselliği doğru aldıktan sonra . O zaman seçicilik mikro optimizasyonlarla kendini ilgilendirir.


Optimizasyonlarla ilgili bir röportaj sorusu olarak, kod olağan tartışmaya neden olur, ancak kodun daha yüksek hedefini özlüyor mu? İşlevsel olarak doğru mu?

Hem C ++ hem de C ve diğerleri, inttaşma sorununu bir problem olarak görüyor a + b. İyi tanımlanmamış ve C tanımsız davranış olarak adlandırıyor . Ortak davranış olsa bile "sarma" olarak belirtilmez.

bool IsSumInRange(int a, int b) {
    int s = a + b;  // Overflow possible
    if (s > 1000 || s < -1000) return false;
    else return true;
}

Çağrılan böyle bir işlevin IsSumInRange()iyi tanımlanmış olması ve tüm intdeğerleri için doğru şekilde çalışması beklenir a,b. Ham a + bdeğil. AC çözümü şunları kullanabilir:

#define N 1000
bool IsSumInRange_FullRange(int a, int b) {
  if (a >= 0) {
    if (b > INT_MAX - a) return false;
  } else {
    if (b < INT_MIN - a) return false;
  }
  int sum = a + b;
  if (sum > N || sum < -N) return false;
  else return true;
}

Yukarıdaki kod daha geniş bir tamsayı türü kullanılarak optimize edilebilir intvarsa aşağıdaki gibi, ya da dağıtmak sum > N, sum < -Niçinde test if (a >= 0)mantık. Bununla birlikte, bu tür optimizasyonlar akıllı bir derleyici tarafından verilen "daha hızlı" yayılan kodlara gerçekten yol açmayabilir veya akıllı olmanın ekstra bakımına değmeyebilir.

  long long sum a;
  sum += b;

Kullanırken bile abs(sum)sorunlara eğilimlidir sum == INT_MIN.


0

Ne tür derleyicilerden bahsediyoruz ve ne tür bir "hafıza"? Örnekte, makul bir eniyileyici varsayarak, ifadenin a+bböyle bir aritmetik işlem yapmadan önce genellikle bir kayıt defterinde (bir bellek şekli) saklanması gerekir.

Dolayısıyla a+b, iki kez karşılaşan aptal bir derleyiciden bahsediyorsak , ikinci örneğinizde daha fazla kayıt (bellek) tahsis edecek , çünkü ilk örneğiniz bu ifadeyi yalnızca yerel değişkenle eşlenen tek bir kayıt defterinde saklayabilir, ancak Bu noktada çok aptalca derleyicilerden bahsediyoruz ... her bir değişkeni her yere döken başka bir aptalca derleyici ile birlikte çalışmadığınız sürece, bu durumda belki birincisi daha fazla üzüntüye neden olur. ikinci*.

Ben hala bunu kazımak istiyorum ve ikincisinin saçma yığılmaya yatkın olsa bile, aptal bir derleyiciyle daha fazla bellek kullanması muhtemel olduğunu düşünüyorum, çünkü üç kayıt için ayırma a+bve dökülme aile sonuçlanabilir b. Biz yakalayan sonra en ilkel optimize edici konuşuyorsak a+biçin smuhtemelen "yardımcı" olacak daha az kayıtları / yığın dökülmeleri kullanın.

Bunların hepsi son derece spekülatiftir, aptalca ölçümler / sökme yapmadan ve en kötü senaryolarda bile, bu bir "hafızaya karşı performans" durumu değildir (çünkü düşünebildiğim en kötü optimize ediciler arasında bile konuşmuyoruz) istifleme / kayıt gibi geçici bellek dışındaki herhangi bir şey hakkında, en iyi ihtimalle "performans" durumudur ve makul olan herhangi bir optimize edici arasında ikisi eşdeğerdir ve eğer makul bir optimize edici kullanılmıyorsa, neden optimizasyonla ilgili saplantılar doğada mikroskobik ve özellikle eksik ölçümler? Bu, yığın seçimi her şeyi dökülen bir tercüman kullanırken, üretken kalmak isteyen hiç kimsenin asla beklemeyeceğim, talimat seçimi / kayıt tahsisi montaj düzeyinde odaklanma gibi bir şey.

Bir yöntem için bellek vs performans hızı ne zaman optimize etmek?

Bu soruya gelince, daha geniş bir şekilde ele alabilirsem, çoğunlukla zıt zıt iki tane bulamıyorum. Özellikle, erişim düzenleriniz sıralıysa ve CPU önbelleğinin hızı düşerse, genellikle önemsiz olmayan girişler için sıralı olarak işlenen bayt miktarındaki bir azalma, bu verileri hızlı bir şekilde toplamaya (bir noktaya kadar) çevrilir. Tabii ki, eğer veriler çok, çok daha fazla talimat karşılığında çok daha küçükse, daha az talimat karşılığında, sırayla daha büyük formda işlem yapmanın daha hızlı olabileceği kırılma noktaları vardır.

Ancak, birçok cihazın bu tür durumlarda bellek kullanımındaki bir azalmanın, harcanan süreçte orantılı azalmaya ne kadar yol açabileceğini hafife alma eğiliminde olduğunu buldum. Performans maliyetlerini, bazı boş hesaplamaları hızlandırmak için büyük LUT'lara ulaşma noktasına bellek erişimi yerine talimatlara dönüştürmek, yalnızca ek bellek erişimi ile düşürülmüş performansı bulmak için çok insanca sezgiseldir.

Bazı büyük dizilerden (örneğin örneğinizdeki gibi yerel skaler değişkenlerden bahsetmiyoruz) ardışık erişim durumları için, sırayla sürükleecek daha az belleğin, özellikle ortaya çıkan kod diğerlerinden daha basit olduğunda, daha yüksek performansa dönüştüğü kuralıyla giderim. 't, ölçümlerim ve profilerim bana aksi söyleyene kadar ve önemli olan, aynı şekilde sırayla diskteki daha küçük bir ikili dosyayı okumanın daha büyük bir dosyadan daha hızlı bir şekilde ilerlemesinin daha hızlı olacağını düşünüyorum (daha küçük olan daha fazla talimat gerektirse bile) ), bu varsayımın benim ölçümlerimde artık geçerli olmadığı gösterilmiştir.

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.