Derleyicinin / iyileştiricinin daha hızlı bir program yapmasını sağlayan Kodlama Uygulamaları


116

Yıllar önce, C derleyicileri pek akıllı değildi. Geçici bir çözüm olarak K&R , derleyiciye bu değişkeni dahili bir kayıtta tutmanın iyi bir fikir olacağını ima etmek için register anahtar kelimesini icat etti . Daha iyi kod üretmeye yardımcı olmak için üçüncül operatörü de yaptılar.

Zaman geçtikçe derleyiciler olgunlaştı. Kayıtlarda hangi değerleri tutacakları konusunda sizin yapabileceğinizden daha iyi kararlar almalarına olanak tanıyan akış analizleri sayesinde çok akıllı hale geldiler. Register anahtar sözcüğü önemsiz hale geldi.

FORTRAN, takma ad sorunları nedeniyle bazı işlemlerde C'den daha hızlı olabilir . Teorik olarak dikkatli kodlamayla, optimize edicinin daha hızlı kod üretmesini sağlamak için bu kısıtlamanın üstesinden gelinebilir.

Derleyicinin / optimize edicinin daha hızlı kod üretmesini sağlayabilecek hangi kodlama uygulamaları mevcuttur?

  • Kullandığınız platformu ve derleyiciyi tanımlamanız takdir edilecektir.
  • Teknik neden işe yarıyor gibi görünüyor?
  • Örnek kod önerilir.

İşte ilgili bir soru

[Düzenle] Bu soru genel olarak profilleme ve optimize etme süreci ile ilgili değildir. Programın doğru yazıldığını, tam optimizasyon ile derlendiğini, test edildiğini ve üretime alındığını varsayalım. Kodunuzda, optimize edicinin yapabileceği en iyi işi yapmasını engelleyen yapılar olabilir. Bu yasakları kaldıracak ve optimize edicinin daha da hızlı kod üretmesine izin verecek şekilde yeniden düzenleme yapmak için ne yapabilirsiniz?

[Düzenle] İlgili bağlantıyı ofsetle


7
Bu (ilginç) soruya 'tek' kesin bir cevap olmadığı için topluluk wiki imho için iyi bir aday olabilir ...
ChristopheD

Her seferinde özlüyorum. Bu konuya işaret ettiğiniz için teşekkür ederiz.
EvilTeach

"Daha iyi" ile basitçe "daha hızlı" mı demek istiyorsunuz yoksa aklınızda başka mükemmellik kriterleri mi var?
Yüksek Performans Mark

1
Özellikle taşınabilir bir şekilde iyi bir kayıt ayırıcısı yazmak oldukça zordur ve kayıt ayırma, performans ve kod boyutu için kesinlikle gereklidir. registeraslında, zayıf derleyicilerle mücadele ederek performansa duyarlı kodu daha taşınabilir hale getirdi.
Potatoswatter

1
@EvilTeach: topluluk wiki "kesin cevap yok" anlamına gelmez, öznel etiketle eşanlamlı değildir. Topluluk wiki, yayınınızı diğer insanların düzenleyebilmesi için topluluğa teslim etmek istediğiniz anlamına gelir. Eğer istemiyorsanız sorularınızı wiki için baskı altında hissetmeyin.
Juliet

Yanıtlar:


54

Yerel değişkenlere yazın, çıktı argümanlarına değil! Bu yavaşlamaların önüne geçmek için büyük bir yardımcı olabilir. Örneğin, kodunuz şöyle görünüyorsa

void DoSomething(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    for (int i=0; i<numFoo, i++)
    {
         barOut.munge(foo1, foo2[i]);
    }
}

derleyici foo1! = barOut olduğunu bilmez ve bu nedenle döngü boyunca her seferinde foo1'i yeniden yüklemesi gerekir. Ayrıca barOut'a yazma bitene kadar foo2 [i] 'yi okuyamaz. Sınırlı işaretçilerle uğraşmaya başlayabilirsiniz, ancak bunu yapmak da aynı derecede etkili (ve çok daha net):

void DoSomethingFaster(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    Foo barTemp = barOut;
    for (int i=0; i<numFoo, i++)
    {
         barTemp.munge(foo1, foo2[i]);
    }
    barOut = barTemp;
}

Kulağa aptalca geliyor, ancak derleyici yerel değişkenle daha akıllıca ilgilenebilir, çünkü bellekte herhangi bir argümanla örtüşmesi mümkün değildir. Bu, korkunç yük çarpma mağazasından kaçınmanıza yardımcı olabilir (bu başlıkta Francis Boivin tarafından bahsedilmiştir).


7
Bunun, programcılar için de çoğu zaman işleri okumayı / anlamayı kolaylaştırma ek faydası vardır, çünkü onlar da olası açık olmayan yan etkiler konusunda endişelenmek zorunda kalmazlar.
Michael Burr

Çoğu IDE varsayılan olarak yerel değişkenleri gösterir, bu nedenle daha az yazı
yazılır

9
bu optimizasyonu kısıtlı işaretçileri kullanarak da etkinleştirebilirsiniz
Ben Voigt

4
@Ben - bu doğru, ama bu yolun daha açık olduğunu düşünüyorum. Ayrıca, girdi ve çıktı çakışırsa, sonucun sınırlı işaretçilerle belirtilmediğine inanıyorum (muhtemelen hata ayıklama ve sürüm arasında farklı davranışlar elde edersiniz), oysa bu yol en azından tutarlı olacaktır. Beni yanlış anlamayın, restrict kullanmayı seviyorum, ama daha fazla ihtiyacım olmamasını seviyorum.
celion

Sadece Foo'nun birkaç meg veriyi kopyalayan tanımlanmış bir kopyalama işlemine sahip olmadığını
ummalısınız

76

Derleyicinin hızlı kod oluşturmasına yardımcı olacak bir kodlama uygulaması - herhangi bir dil, herhangi bir platform, herhangi bir derleyici, herhangi bir sorun:

Do not hangi kuvvet herhangi zekice hileler kullanmak, hatta en iyi düşünüyorum (önbellek ve kayıtları dahil) bellekte değişkenleri ortaya koymak, derleyici teşvik ediyoruz. Önce doğru ve sürdürülebilir bir program yazın.

Ardından, kodunuzun profilini çıkarın.

Daha sonra ve ancak o zaman, derleyiciye belleği nasıl kullanacağını söylemenin etkilerini araştırmaya başlamak isteyebilirsiniz. Bir seferde 1 değişiklik yapın ve etkisini ölçün.

Hayal kırıklığına uğramayı ve küçük performans iyileştirmeleri için gerçekten çok çalışmak zorunda kalmayı bekleyin. Fortran ve C gibi olgun diller için modern derleyiciler çok ama çok iyidir. Koddan daha iyi performans elde etmek için bir 'hile' hesabını okursanız, derleyici yazarlarının da bunu okuduğunu ve yapmaya değecekse muhtemelen uyguladığını unutmayın. Muhtemelen ilk başta okuduklarınızı yazmışlardır.


20
Compiier geliştiricilerinin, tıpkı herkes gibi sınırlı bir zamanı vardır. Tüm optimizasyonlar onu derleyiciye sağlamaz. &İkinin %gücüne karşı olduğu gibi (nadiren optimize edilir, ancak önemli performans etkileri olabilir). Performans için bir numara okursanız, işe yarayıp yaramadığını anlamanın tek yolu değişikliği yapmak ve etkisini ölçmektir. Derleyicinin sizin için bir şeyi optimize edeceğini asla varsaymayın.
Dave Jarvis

22
& ve%, diğer çoğu ucuz aritmetik hilelerle birlikte hemen hemen her zaman optimize edilir. Optimize edilmeyen şey, sağ taraftaki işlenenin her zaman ikinin kuvveti olan bir değişken olması durumudur.
Potatoswatter

8
Açıklığa kavuşturmak gerekirse, bazı okuyucuların kafasını karıştırmış gibi görünüyorum: Önerdiğim kodlama uygulamasındaki tavsiye, ilk olarak, performansın temelini oluşturmak için bellek düzeni talimatlarını kullanmayan basit bir kod geliştirmektir. Ardından, her şeyi birer birer deneyin ve etkilerini ölçün. Operasyonların performansı konusunda herhangi bir tavsiye vermedim.
Yüksek Performans Mark

17
Sabit güç-ikide ngcc , optimizasyon devre dışı bırakıldığında bile% n ile değiştirilir . Bu tam olarak "nadiren, hiç değilse" değil ...& (n-1)
Porculus

12
% CAN olarak optimize edilebilir ve tipi (oldukça olumlu kalan sahip her zaman aşağı yuvarlama ve daha 0 doğru yuvarlak ve negatif geri kalan kısmı) negatif tamsayı bölümü için C'nin saçma kurallar nedeniyle, imzalandığı. Ve çoğu zaman, cahil kodlayıcılar işaretli tipler kullanırlar ...
R .. GitHub BUZ YARDIMINI DURDUR

47

Hafızadan geçtiğiniz sıranın performans üzerinde derin etkileri olabilir ve derleyiciler bunu çözme ve düzeltme konusunda pek iyi değildir. Performansı önemsiyorsanız, kod yazarken önbellek yerelliği konusunda dikkatli olmalısınız. Örneğin, C'deki iki boyutlu diziler ana satır biçiminde tahsis edilir. Sütun ana biçimindeki dizileri gezmek, daha fazla önbellek eksikliğine sahip olmanıza ve programınızı işlemciye bağlı olduğundan daha fazla belleğe bağlı hale getirme eğilimindedir:

#define N 1000000;
int matrix[N][N] = { ... };

//awesomely fast
long sum = 0;
for(int i = 0; i < N; i++){
  for(int j = 0; j < N; j++){
    sum += matrix[i][j];
  }
}

//painfully slow
long sum = 0;
for(int i = 0; i < N; i++){
  for(int j = 0; j < N; j++){
    sum += matrix[j][i];
  }
}

Açıkçası bu bir optimizasyon sorunu değil, bir optimizasyon sorunu.
EvilTeach

10
Elbette bu bir optimize edici sorun. İnsanlar onlarca yıldır otomatik döngü değişim optimizasyonu hakkında makaleler yazıyorlar.
Phil Miller

20
@Potatoswatter Neden bahsediyorsun? C derleyicisi, aynı nihai sonuç gözlemlendiği sürece istediği her şeyi yapabilir ve gerçekten de GCC 4.4, -floop-interchangeeğer optimize edicinin karlı olduğunu düşünürse bir iç ve dış döngüyü çevirecektir.
efemient

2
Huh, işte oraya gidin. C semantiği genellikle diğer adlandırma sorunlarıyla gölgelenir. Sanırım buradaki gerçek tavsiye o bayrağı uzatmaktır!
Patates suyu

36

Genel Optimizasyonlar

En sevdiğim optimizasyonlardan bazıları burada. Bunları kullanarak yürütme sürelerini artırdım ve program boyutlarını azalttım.

Küçük işlevleri inlinemakro olarak bildirin

Bir işleve (veya yönteme) yapılan her çağrı, değişkenleri yığına göndermek gibi ek yüklere neden olur. Bazı işlevler de dönüşte ek yüke neden olabilir. Verimsiz bir işlev veya yöntem, içeriğinde birleşik ek yükten daha az ifadeye sahiptir. Bunlar inlineing için iyi adaylardır.#defineMakrolar veya inlinefonksiyonlar . (Evet, inlinesadece bir öneri olduğunu biliyorum , ancak bu durumda bunu derleyiciye bir hatırlatma olarak kabul ediyorum .)

Ölü ve gereksiz kodu kaldırın

Kod kullanılmazsa veya programın sonucuna katkıda bulunmazsa, ondan kurtulun.

Algoritma tasarımını basitleştirin

Bir keresinde, hesapladığı cebirsel denklemi yazarak bir programdan çok sayıda montaj kodunu ve yürütme süresini kaldırdım ve ardından cebirsel ifadeyi basitleştirdim. Basitleştirilmiş cebirsel ifadenin uygulanması, orijinal fonksiyondan daha az yer ve zaman aldı.

Döngü Açılıyor

Her döngü, bir artırma ve sonlandırma denetimi ek yüküne sahiptir. Performans faktörünün bir tahminini elde etmek için, ek yükteki talimatların sayısını sayın (minimum 3: artış, kontrol, döngünün başlangıcına git) ve döngü içindeki ifade sayısına bölün. Sayı ne kadar düşükse o kadar iyidir.

Düzenleme: Önce:

unsigned int sum = 0;
for (size_t i; i < BYTES_TO_CHECKSUM; ++i)
{
    sum += *buffer++;
}

Kaydırdıktan sonra:

unsigned int sum = 0;
size_t i = 0;
**const size_t STATEMENTS_PER_LOOP = 8;**
for (i = 0; i < BYTES_TO_CHECKSUM; **i = i / STATEMENTS_PER_LOOP**)
{
    sum += *buffer++; // 1
    sum += *buffer++; // 2
    sum += *buffer++; // 3
    sum += *buffer++; // 4
    sum += *buffer++; // 5
    sum += *buffer++; // 6
    sum += *buffer++; // 7
    sum += *buffer++; // 8
}
// Handle the remainder:
for (; i < BYTES_TO_CHECKSUM; ++i)
{
    sum += *buffer++;
}

Bu avantajda, ikincil bir fayda elde edilir: İşlemcinin talimat önbelleğini yeniden yüklemesi gerekmeden önce daha fazla ifade yürütülür.

Bir döngüyü 32 ifadeye açtığımda harika sonuçlar elde ettim. Bu, programın 2GB'lık bir dosyada bir sağlama toplamı hesaplaması gerektiğinden, darboğazlardan biriydi. Bu optimizasyon, blok okuma ile birleştiğinde performansı 1 saatten 5 dakikaya yükseltti. Döngü açma, assembly dilinde de mükemmel performans sağladı, benim memcpyderleyicininkinden çok daha hızlıydı memcpy. - TM

ifİfadelerin azaltılması

İşlemciler, işlemciyi talimat sırasını yeniden yüklemeye zorladığı için dallardan veya atlamalardan nefret eder.

Boole Aritmetiği ( Düzenlendi: kod parçasına uygulanan kod formatı, örnek eklendi)

ifİfadeleri boole atamalarına dönüştürün . Bazı işlemciler, dallanma olmaksızın talimatları koşullu olarak yürütebilir:

bool status = true;
status = status && /* first test */;
status = status && /* second test */;

Kısa devre arasında mantıksal ve (operatör &&eğer varsa) testlerin uygulanmasını engellemektedir statusolanfalse .

Misal:

struct Reader_Interface
{
  virtual bool  write(unsigned int value) = 0;
};

struct Rectangle
{
  unsigned int origin_x;
  unsigned int origin_y;
  unsigned int height;
  unsigned int width;

  bool  write(Reader_Interface * p_reader)
  {
    bool status = false;
    if (p_reader)
    {
       status = p_reader->write(origin_x);
       status = status && p_reader->write(origin_y);
       status = status && p_reader->write(height);
       status = status && p_reader->write(width);
    }
    return status;
};

Döngülerin dışında Faktör Değişken Tahsisi

Döngü içinde anında bir değişken oluşturulursa, oluşturma / tahsis işlemini döngünün öncesine taşıyın. Çoğu durumda, değişkenin her yineleme sırasında tahsis edilmesi gerekmez.

Döngülerin dışındaki faktör sabiti ifadeleri

Bir hesaplama veya değişken değeri döngü indeksine bağlı değilse, onu döngünün dışına (öncesinde) taşıyın.

Bloklar halinde I / O

Verileri büyük parçalar halinde (bloklar) okuyun ve yazın. Daha büyük daha iyi. Örneğin, bir seferde bir sekizli okumak, tek okumayla 1024 sekizli okumaktan daha az verimlidir.
Misal:

static const char  Menu_Text[] = "\n"
    "1) Print\n"
    "2) Insert new customer\n"
    "3) Destroy\n"
    "4) Launch Nasal Demons\n"
    "Enter selection:  ";
static const size_t Menu_Text_Length = sizeof(Menu_Text) - sizeof('\0');
//...
std::cout.write(Menu_Text, Menu_Text_Length);

Bu tekniğin etkinliği görsel olarak gösterilebilir. :-)

Sürekli veri için printf aileyi kullanmayın

Sabit veri, blok yazma kullanılarak çıkarılabilir. Biçimlendirilmiş yazma, karakterleri biçimlendirmek veya biçimlendirme komutlarını işlemek için metni tararken zaman kaybına neden olur. Yukarıdaki kod örneğine bakın.

Belleğe formatlayın, sonra yazın

charBirden çok kullanarak bir dizi biçimlendirin sprintf, sonra kullanın fwrite. Bu aynı zamanda veri düzeninin "sabit bölümlere" ve değişken bölümlere ayrılmasına izin verir. Adres mektup birleştirmeyi düşünün .

Sabit metni (dize değişmezleri) olarak bildir static const

Değişkenler olmadan bildirildiğinde static, bazı derleyiciler yığın üzerinde alan ayırabilir ve verileri ROM'dan kopyalayabilir. Bunlar iki gereksiz işlemdir. Bu, staticönek kullanılarak düzeltilebilir .

Son olarak, derleyicinin yapacağı gibi kod

Bazen derleyici birkaç küçük ifadeyi bir karmaşık sürümden daha iyi optimize edebilir. Ayrıca, derleyicinin optimize etmesine yardımcı olacak kod yazmak da yardımcı olur. Derleyicinin özel blok transfer talimatlarını kullanmasını istersem, özel talimatları kullanması gerektiği gibi görünen bir kod yazacağım.


2
İlginç olan, daha büyük bir ifade yerine birkaç küçük ifadeyle daha iyi kod aldığınız bir örnek verebilir misiniz? Boole kullanarak bir if yazımının bir örneğini gösterebilir misiniz? Genel olarak, önbellek boyutu için muhtemelen daha iyi bir hisse sahip olduğundan, döngüyü derleyiciye açılırken bırakırdım. Sprintfing ve ardından fwriting fikrine biraz şaşırdım. Ben fprintf'in bunu aslında kaputun altında yaptığını düşünürdüm. Burada biraz daha ayrıntı verebilir misiniz?
EvilTeach

1
fprintfAyrı bir arabelleğe biçimlendirip ardından arabelleği çıktı olarak vereceğinin garantisi yoktur . Aerodinamik (bellek kullanımı için) fprintf, tüm biçimlendirilmemiş metni çıktılar, sonra biçimlendirir ve çıktı alır ve tüm biçim dizesi işlenene kadar tekrar eder, böylece her çıktı türü için (biçimlendirilmiş ve biçimlendirilmemiş) 1 çıktı çağrısı yapar. Diğer uygulamaların, tüm yeni dizeyi tutması için her çağrı için dinamik olarak bellek ayırması gerekir (bu, gömülü sistemler ortamında kötüdür). Önerim çıktı sayısını azaltır.
Thomas Matthews

3
Bir zamanlar bir döngü oluşturarak önemli bir performans artışı elde ettim. Sonra biraz dolaylı yoldan nasıl daha sıkı sarılacağını anladım ve program fark edilir şekilde hızlandı. (Profil oluşturma, bu özel işlevin çalışma süresinin% 60-80'i olduğunu gösterdi ve performansı öncesinde ve sonrasında dikkatlice test ettim.) İyileşmenin daha iyi yerellikten kaynaklandığına inanıyorum, ancak bundan tam olarak emin değilim.
David Thornley

16
Bunların çoğu, programcıların derleyiciye optimizasyona yardımcı olma yollarından çok programcı optimizasyonlarıdır; bu, orijinal sorunun itici gücüdür. Örneğin, döngü açma. Evet, kendi açılımınızı yapabilirsiniz, ancak bence derleyicinin sizin için açması ve kaldırması için ne gibi engeller olduğunu bulmak daha ilginç.
Adrian McCarthy

26

İyileştirici, programınızın performansını gerçekten kontrol etmiyor, siz öylesiniz. Uygun algoritmaları ve yapıları ve profili, profili, profili kullanın.

Bununla birlikte, başka bir dosyadaki bir dosyadaki küçük bir işlevde iç döngü yapmamalısınız, çünkü bu onun satır içi olmasını durdurur.

Mümkünse bir değişkenin adresini almaktan kaçının. Bir işaretçi istemek, değişkenin bellekte tutulması gerektiği anlamına geldiğinden "ücretsiz" değildir. İşaretçilerden kaçınırsanız, bir dizi bile kayıtlarda tutulabilir - bu, vektörleştirme için gereklidir.

Bu da bir sonraki noktaya götürür , ^ # $ @ kılavuzunu okuyun ! __restrict__Buraya ve __attribute__( __aligned__ )oraya serpiştirirseniz GCC düz C kodunu vektörleştirebilir . İyileştiriciden çok özel bir şey istiyorsanız, spesifik olmanız gerekebilir.


14
Bu iyi bir cevap, ancak tüm program optimizasyonunun daha popüler hale geldiğini ve aslında çeviri birimlerinde satır içi işlevlerin kullanılabileceğini unutmayın.
Phil Miller

1
@Novelocrat Yep - Söylemeye gerek yok, ilk kez bir şeyin A.csatır içine girdiğini gördüğümde çok şaşırdım B.c.
Jonathon Reinhart

18

Çoğu modern işlemcide, en büyük darboğaz bellektir.

Takma ad: Load-Hit-Store, dar bir döngüde yıkıcı olabilir. Bir bellek konumunu okuyor ve diğerine yazıyorsanız ve bunların ayrık olduğunu biliyorsanız, işlev parametrelerine dikkatlice bir takma ad anahtar sözcüğü koymak, derleyicinin daha hızlı kod oluşturmasına gerçekten yardımcı olabilir. Bununla birlikte, bellek bölgeleri çakışırsa ve 'takma ad' kullandıysanız, tanımlanmamış davranışlar için iyi bir hata ayıklama oturumu içindesiniz!

Önbellek kaçırma: Çoğunlukla algoritmik olduğu için derleyiciye nasıl yardım edebileceğinizden tam olarak emin değilsiniz, ancak belleği önceden getirmenin temelleri var.

Ayrıca farklı yazmaçlar kullandıklarından ve bir türden diğerine dönüştürme gerçek dönüştürme talimatını çağırmak, değeri belleğe yazmak ve uygun yazmaç kümesinde geri okumak anlamına geldiğinden kayan nokta değerlerini int'e ve tersini çok fazla dönüştürmeye çalışmayın. .


4
Yük vuran mağazalar ve farklı kayıt türleri için +1. X86'da ne kadar büyük bir anlaşma olduğundan emin değilim, ancak PowerPC'yi (örneğin Xbox360 ve Playstation3) planlıyorlar.
celion

Derleyici döngü optimizasyon tekniklerindeki çoğu makale, mükemmel iç içe yerleştirmeyi varsayar; bu, en içteki hariç her döngünün gövdesinin yalnızca başka bir döngü olduğu anlamına gelir. Bu makaleler, olabileceği çok açık olsa bile, genelleştirmek için gerekli adımları tartışmıyor. Bu nedenle, gereken ekstra çaba nedeniyle birçok uygulamanın bu genellemeleri desteklememesini bekliyorum. Bu nedenle, döngülerde önbellek kullanımını optimize etmeye yönelik birçok algoritma, mükemmel yuvalarda kusurlu yuvalara göre çok daha iyi çalışabilir.
Phil Miller

11

İnsanların yazdığı kodun büyük çoğunluğu G / Ç bağımlı olacaktır (son 30 yılda para için yazdığım tüm kodların çok bağlı olduğuna inanıyorum), bu nedenle çoğu insan için optimizasyonun faaliyetleri akademik olacaktır.

Bununla birlikte, insanlara, kodun optimize edilmesi için derleyiciye onu optimize etmesini söylemeniz gerektiğini hatırlatmak isterim - pek çok kişi (unuttuğumda ben de dahil olmak üzere), iyileştirici etkinleştirilmeden anlamsız olan C ++ kıyaslamaları burada yayınlıyor.


7
Garip olduğumu itiraf ediyorum - hafıza bant genişliğine bağlı büyük bilimsel sayı hesaplama kodları üzerinde çalışıyorum. Genel program popülasyonu için Neil ile aynı fikirdeyim.
Yüksek Performans Mark

6
Doğru; ancak günümüzde bu G / Ç'ye bağlı kodun çok büyük bir kısmı pratikte karamsar olan dillerde - derleyicilere bile sahip olmayan dillerde yazılıyor . C ve C ++ 'nın hala kullanıldığı alanların, bir şeyi optimize
etmenin

3
Son 30 yılın çoğunu çok az G / Ç ile kod üzerinde çalışarak geçirdim. Veritabanları yaparak 2 yıl tasarruf edin. Grafikler, kontrol sistemleri, simülasyon - hiçbiri G / Ç bağlı. G / Ç çoğu insanın darboğazı olsaydı, Intel ve AMD'ye fazla ilgi göstermezdik.
phkahler

2
Evet, bu argümana gerçekten inanmıyorum - aksi takdirde biz (benim işimde) hesaplama zamanının çoğunu G / Ç yaparak daha fazla harcamanın yollarını aramazdık. Ayrıca, karşılaştığım G / Ç bağlantılı yazılımların çoğu G / Ç bağlandı çünkü G / Ç dikkatsizce yapıldı; erişim modellerini optimize ederseniz (tıpkı bellek gibi), performansta büyük kazançlar elde edilebilir.
dash-tom-bang

3
Yakın zamanda, C ++ dilinde yazılmış neredeyse hiçbir kodun G / Ç'ye bağlı olmadığını keşfettim. Elbette, toplu disk aktarımı için bir işletim sistemi işlevi arıyorsanız, iş parçacığınız G / Ç beklemeye gidebilir (ancak önbelleğe alma ile, bu şüpheli olsa bile). Ancak, standart ve taşınabilir oldukları için herkesin önerdiği alışılagelmiş G / Ç kütüphane işlevleri, aslında modern disk teknolojisine (makul fiyatlı şeyler bile) kıyasla çok yavaştır. Büyük olasılıkla, G / Ç, yalnızca birkaç bayt yazdıktan sonra diske kadar tüm yolu temizliyorsanız darboğazdır. OTOH, UI farklı bir konu, biz insanlar yavaşız.
Ben Voigt

11

kodunuzda olabildiğince const doğruluğunu kullanın. Derleyicinin çok daha iyi optimize etmesini sağlar.

Bu belgede birçok başka optimizasyon ipucu bulunmaktadır: CPP optimizasyonları (biraz eski bir belge olsa da)

vurgular:

  • yapıcı başlatma listelerini kullan
  • önek operatörlerini kullan
  • açık oluşturucular kullanın
  • satır içi işlevler
  • geçici nesnelerden kaçının
  • sanal işlevlerin maliyetinin farkında olun
  • referans parametreleri aracılığıyla nesneleri döndür
  • sınıf başına tahsis
  • stl konteyner ayırıcılarını düşünün
  • 'boş üye' optimizasyonu
  • vb

8
Çok değil, nadiren. Yine de gerçek doğruluğu artırıyor.
Potatoswatter

5
C ve C ++ 'da derleyici iyileştirmek için const kullanamaz çünkü onu dışarı atmak iyi tanımlanmış bir davranıştır.
dsimcha

+1: const, derlenen kodu doğrudan etkileyecek bir şeye iyi bir örnektir. re @ dsimcha'nın yorumu - iyi bir derleyici bunun olup olmadığını test eder. Elbette, iyi bir derleyici zaten bu şekilde bildirilmeyen const öğelerini "bulacaktır" ...
Hogan

@dsimcha: Değişen const ve restrict nitelikli işaretçi Ancak tanımlanmamış. Böylece bir derleyici böyle bir durumda farklı şekilde optimize edebilir.
Dietrich Epp

6
@dsimcha nesneye olmayan constbir constreferans veya constişaretçiye çevrim constiyi tanımlanmıştır. gerçek bir constnesneyi değiştirmek (yani constorijinal olarak bildirilen ) değildir.
Stephen Lin

9

Mümkün olduğunca statik tek atama kullanarak programlamaya çalışın. SSA, çoğu işlevsel programlama dilinde elde ettiğinizle tamamen aynıdır ve çoğu derleyicinin kodunuzu optimizasyonlarını yapmak için dönüştürdüğü şey budur çünkü çalışmak daha kolaydır. Derleyicinin kafasının karışabileceği bu yerler gün ışığına çıkarılır. Ayrıca, en kötü kayıt ayırıcıları hariç tümünün en iyi kayıt ayırıcıları kadar iyi çalışmasını sağlar ve daha kolay hata ayıklamanıza izin verir, çünkü bir değişkenin değerini nereden aldığını merak etmek zorunda kalmazsınız, çünkü atandığı tek bir yer vardır.
Global değişkenlerden kaçının.

Referans veya işaretçiye göre verilerle çalışırken, bunu yerel değişkenlere çekin, işinizi yapın ve ardından geri kopyalayın. (yapmamak için iyi bir nedeniniz yoksa)

Çoğu işlemcinin matematik veya mantık işlemleri yaparken size verdiği neredeyse ücretsiz karşılaştırmadan yararlanın. Neredeyse her zaman == 0 ve <0 için bir bayrak alırsınız, buradan kolayca 3 koşulu elde edebilirsiniz:

x= f();
if(!x){
   a();
} else if (x<0){
   b();
} else {
   c();
}

neredeyse her zaman diğer sabitleri test etmekten daha ucuzdur.

Diğer bir hile, aralık testinde bir karşılaştırmayı ortadan kaldırmak için çıkarma kullanmaktır.

#define FOO_MIN 8
#define FOO_MAX 199
int good_foo(int foo) {
    unsigned int bar = foo-FOO_MIN;
    int rc = ((FOO_MAX-FOO_MIN) < bar) ? 1 : 0;
    return rc;
} 

Bu, genellikle boole ifadelerinde kısa devre yapan dillerde bir sıçramayı önleyebilir ve derleyicinin, ikinciyi yaparken ve sonra bunları birleştirirken ilk karşılaştırmanın sonucuna nasıl ayak uyduracağını bulmaya çalışmak zorunda kalmasını önler. Bu, fazladan bir kayıt kullanma potansiyeline sahip gibi görünebilir, ancak neredeyse hiç kullanmaz. Genellikle artık foo'ya ihtiyacınız yoktur ve eğer yaparsanız rc henüz kullanılmadığından oraya gidebilir.

C (strcpy, memcpy, ...) içindeki dize işlevlerini kullanırken ne döndüklerini hatırlayın - hedef! Hedefe giden işaretçi kopyanızı 'unutarak' daha iyi bir kod elde edebilirsiniz ve bu işlevlerin dönüşünden sonra onu geri alabilirsiniz.

Döndürdüğünüz son işlevin tam olarak aynı şeyi döndürme fırsatını asla gözden kaçırmayın. Derleyiciler bunu anlamakta pek iyi değil:

foo_t * make_foo(int a, int b, int c) {
        foo_t * x = malloc(sizeof(foo));
        if (!x) {
             // return NULL;
             return x; // x is NULL, already in the register used for returns, so duh
        }
        x->a= a;
        x->b = b;
        x->c = c;
        return x;
}

Elbette, bunun mantığını tersine çevirebilirsiniz, ancak ve yalnızca bir dönüş noktasına sahip olabilirsiniz.

(daha sonra hatırladığım numaralar)

Yapabildiğiniz zaman fonksiyonları statik olarak bildirmek her zaman iyi bir fikirdir. Derleyici, belirli bir işlevin her arayanını hesaba kattığını kendi kendine kanıtlayabilirse, optimizasyon adına bu işlev için çağrı kurallarını bozabilir. Derleyiciler genellikle parametrelerin kayıtlara veya fonksiyonlar olarak adlandırılan yığın pozisyonlarına taşınmasından kaçınabilirler (bunu yapmak için hem çağrılan fonksiyonda hem de çağıranların konumunda sapması gerekir). Derleyici ayrıca, çağrılan işlevin hangi belleğe ve kayıtlara ihtiyaç duyacağını bilmekten ve çağrılan işlevin rahatsız etmediği yazmaçlarda veya bellek konumlarında bulunan değişken değerleri korumak için kod üretmekten kaçınabilir. Bu, özellikle bir işleve çok az çağrı olduğunda işe yarar.


2
Aralıkları test ederken çıkarmayı kullanmak aslında gerekli değil, LLVM, GCC ve derleyicim en azından bunu otomatik olarak yapıyor. Muhtemelen çok az insan çıkarma ile kodun ne yaptığını ve hatta neden gerçekten işe yaradığını anlayabilir.
Gratianus Lup

Yukarıdaki örnekte, b () çağrılamaz çünkü eğer (x <0) ise a () çağrılacaktır.
EvilTeach

@EvilTeach Hayır olmayacak. A () çağrısıyla sonuçlanan karşılaştırma! X
nategoose

@nategoose. x -3 ise o zaman! x doğrudur.
EvilTeach

@EvilTeach In C 0 yanlış ve diğer her şey doğru, yani -3 doğru, yani! -3 yanlış
nategoose

9

Optimize edici bir C derleyicisi yazdım ve işte dikkate alınması gereken çok yararlı şeyler:

  1. Çoğu işlevi statik hale getirin. Bu, prosedürler arası sabit yayılma ve takma ad analizinin işini yapmasına izin verir, aksi takdirde derleyicinin, işlevin, parametreler için tamamen bilinmeyen değerlerle çeviri biriminin dışından çağrılabileceğini varsayması gerekir. İyi bilinen açık kaynaklı kitaplıklara bakarsanız, gerçekten extern olması gerekenler dışında hepsi statik olarak işaretler.

  2. Global değişkenler kullanılıyorsa, bunları mümkünse statik ve sabit olarak işaretleyin. Bir kez başlatılırlarsa (salt okunur), statik const int VAL [] = {1,2,3,4} gibi bir başlatıcı listesi kullanmak daha iyidir, aksi takdirde derleyici değişkenlerin gerçekten başlatılmış sabitler olduğunu keşfetmeyebilir ve değişkendeki yükleri sabitlerle değiştirmede başarısız olur.

  3. Bir döngünün içine ASLA bir gitme, döngü artık çoğu derleyici tarafından tanınmayacak ve en önemli optimizasyonların hiçbiri uygulanmayacaktır.

  4. İşaretçi parametrelerini yalnızca gerekliyse kullanın ve mümkünse kısıtlı olarak işaretleyin. Bu, diğer ad analizine çok yardımcı olur çünkü programcı, herhangi bir takma ad olmadığını garanti eder (işlemler arası takma ad analizi genellikle çok ilkeldir). Çok küçük yapı nesneleri referans olarak değil, değere göre aktarılmalıdır.

  5. Mümkün olduğunda, özellikle döngülerin (a [i]) içinde işaretçiler yerine dizileri kullanın. Bir dizi genellikle diğer ad analizi için daha fazla bilgi sunar ve bazı optimizasyonlardan sonra yine de aynı kod üretilir (merak ediyorsanız döngü gücü azaltma arayın). Bu ayrıca döngüde değişmeyen kod hareketinin uygulanma şansını da artırır.

  6. Yan etkileri olmayan (mevcut döngü yinelemesine bağlı değildir) büyük işlevlere veya harici işlevlere döngü çağrılarının dışına çıkmaya çalışın. Küçük işlevler çoğu durumda satır içi olarak dizilir veya kaldırılması kolay içsel özelliklere dönüştürülür, ancak büyük işlevler derleyicinin aslında yan etkileri olmadığı halde yan etkilere sahip gibi görünebilir. Harici işlevlerin yan etkileri, standart kitaplıktaki bazı derleyiciler tarafından bazen modellenen ve döngü ile değişmeyen kod hareketini mümkün kılan bazı işlevler dışında, tamamen bilinmemektedir.

  7. Birden fazla koşul içeren testler yazarken en olası olanı ilk sıraya koyun. if (a || b || c) olmalıdır if (b || a || c) if b'nin diğerlerine göre doğru olma olasılığı daha yüksektir. Derleyiciler genellikle koşulların olası değerleri ve hangi dalların daha fazla alındığı hakkında hiçbir şey bilmezler (profil bilgileri kullanılarak bilinebilirler, ancak çok az programcı kullanır).

  8. Anahtar kullanmak if (a || b || ... || z) gibi bir test yapmaktan daha hızlıdır. Önce derleyicinizin bunu otomatik olarak yapıp yapmadığını kontrol edin, bazıları yapar ve eğer olsa olsa daha okunabilir .


7

Gömülü sistemler ve C / C ++ ile yazılmış kod söz konusu olduğunda, dinamik bellek ayırmayı denerim ve kaçınırım mümkün olduğunca . Bunu yapmamın ana sebebi mutlaka performans değil, ancak bu temel kuralın performans etkileri var.

Yığını yönetmek için kullanılan algoritmalar, bazı platformlarda (örneğin, vxworks) oldukça yavaştır. Daha da kötüsü, bir çağrıdan malloc'a dönmek için gereken süre, büyük ölçüde yığının mevcut durumuna bağlıdır. Bu nedenle, malloc'u çağıran herhangi bir işlev, kolayca hesaplanamayacak bir performans darbesi alacaktır. Yığın hala temizse, bu performans vuruşu minimum olabilir, ancak bu cihaz bir süre çalıştıktan sonra yığın parçalanabilir. Görüşmeler daha uzun sürecek ve performansın zaman içinde nasıl düşeceğini kolayca hesaplayamazsınız. Gerçekten daha kötü bir durum tahmini üretemezsiniz. İyileştirici bu durumda da size herhangi bir yardım sağlayamaz. Sorunları daha da kötüleştirmek için, eğer yığın çok fazla parçalanırsa, çağrılar tamamen başarısız olmaya başlayacaktır. Çözüm, bellek havuzlarını kullanmaktır (ör.glib dilimleri ) yığın yerine. Doğru yaparsanız tahsis aramaları çok daha hızlı ve belirleyici olacaktır.


Benim pratik kuralım, dinamik olarak ayırmanız gerekiyorsa, bir dizi elde edin, böylece tekrar yapmanıza gerek kalmaz. Onları vektörleri önceden tahsis edin.
EvilTeach

7

Küçük aptalca bir ipucu, ama size biraz hız ve kod tasarrufu sağlayacak.

Her zaman işlev bağımsız değişkenlerini aynı sırada iletin.

F_2'yi çağıran f_1 (x, y, z) varsa, f_2'yi f_2 (x, y, z) olarak bildirin. Bunu f_2 (x, z, y) olarak bildirmeyin.

Bunun nedeni, C / C ++ platformu ABI'nin (AKA çağırma kuralı) belirli kayıtlarda ve yığın konumlarında bağımsız değişkenleri geçirmeyi vaat etmesidir. Argümanlar zaten doğru kayıtlarda olduğunda, onları hareket ettirmesi gerekmez.

Demonte edilmiş kodu okurken, insanlar bu kurala uymadığı için bazı saçma kayıtların karıştırıldığını gördüm.


2
Ne C ne de C ++, belirli yazmaçları veya yığın konumlarını geçme konusunda herhangi bir garanti vermez ve hatta bunlardan bahsetmez. Bu var ABI parametre geçişi ayrıntılarını belirleyen (örn Linux ELF).
Emmet

5

Yukarıdaki listede görmediğim iki kodlama tekniği:

Benzersiz bir kaynak olarak kod yazarak bağlayıcıyı atlayın

Ayrı derleme, derleme zamanı için gerçekten güzel olsa da, optimizasyondan bahsettiğinizde çok kötü. Temel olarak derleyici, derleme biriminin, yani bağlayıcıya ayrılmış etki alanının ötesinde optimizasyon yapamaz.

Ancak, programınızı iyi tasarlarsanız, onu benzersiz bir ortak kaynak aracılığıyla da derleyebilirsiniz. Bu, unit1.c ve unit2.c'yi derlemek yerine, daha sonra her iki nesneyi de bağlayın, yalnızca unit1.c ve unit2.c'yi # içeren all.c'yi derleyin. Böylece tüm derleyici optimizasyonlarından yararlanacaksınız.

Bu, yalnızca C ++ programlarında başlık yazmak gibidir (ve C'de yapmak daha da kolaydır).

Bu teknik, programınızı baştan etkinleştirecek şekilde yazarsanız yeterince kolaydır, ancak C anlambiliminin bir bölümünü değiştirdiğini ve statik değişkenler veya makro çarpışması gibi bazı sorunlarla karşılaşabileceğinizi de bilmelisiniz. Çoğu program için, ortaya çıkan küçük sorunların üstesinden gelmek yeterince kolaydır. Ayrıca, benzersiz bir kaynak olarak derlemenin çok daha yavaş olduğunu ve çok fazla bellek gerektirebileceğini unutmayın (genellikle modern sistemlerde bir sorun değildir).

Bu basit tekniği kullanarak yazdığım bazı programları on kat daha hızlı yaptım!

Register anahtar kelimesi gibi, bu numara da yakında geçersiz hale gelebilir. Bağlayıcı aracılığıyla optimizasyon , derleyiciler gcc: Bağlantı süresi optimizasyonu tarafından desteklenmeye başlar .

Döngülerde ayrı atomik görevler

Bu daha zor. Algoritma tasarımı ile optimize edicinin önbelleği yönetme ve kayıt ayırma yöntemi arasındaki etkileşimle ilgilidir. Çoğu zaman programların bazı veri yapıları üzerinde döngü yapması gerekir ve her öğe için bazı eylemler gerçekleştirir. Çoğunlukla gerçekleştirilen eylemler mantıksal olarak bağımsız iki görev arasında bölünebilir. Bu durumda, tam olarak aynı programı, aynı sınır üzerinde tam olarak bir görevi gerçekleştiren iki döngü ile yazabilirsiniz. Bazı durumlarda, bu şekilde yazmak, benzersiz döngüden daha hızlı olabilir (ayrıntılar daha karmaşıktır, ancak bir açıklama, basit görev durumuyla tüm değişkenlerin işlemci kayıtlarında tutulabileceği ve daha karmaşık olanla bunun mümkün olmadığı ve bazılarının kayıtlar belleğe yazılmalı ve daha sonra tekrar okunmalıdır ve maliyeti ek akış kontrolünden daha yüksektir).

Buna dikkat edin (bu numarayı kullanan veya kullanmayan profil performansları), çünkü kayıt kullanmak gibi gelişmiş olanlardan daha az performans da verebilir.


2
Evet, şimdiye kadar LTO, bu gönderinin ilk yarısını gereksiz ve muhtemelen kötü bir tavsiye haline getirdi.
underscore_d

@underscore_d: hala bazı sorunlar var (çoğunlukla dışa aktarılan sembollerin görünürlüğüyle ilgili), ancak yalnızca performans açısından bakıldığında muhtemelen artık hiçbir şey yok.
kriss

4

Aslında bunun SQLite'da yapıldığını gördüm ve performans artışının yaklaşık% 5 ile sonuçlandığını iddia ediyorlar: Tüm kodunuzu tek bir dosyaya koyun veya bunun eşdeğerini yapmak için ön işlemciyi kullanın. Bu şekilde, optimizer tüm programa erişebilir ve daha fazla prosedürler arası optimizasyon yapabilir.


5
Yakın fiziksel yakınlıkta birlikte kullanılan işlevleri kaynağa koymak, nesne dosyalarında birbirine yakın ve yürütülebilir dosyanızda birbirine yakın olma olasılığını artırır. Talimatların bu iyileştirilmiş konumu, çalışırken talimat önbelleğinin kaçırılmasını önlemeye yardımcı olabilir.
paxos1977

AIX derleyicisinin bu davranışı teşvik etmek için bir derleyici anahtarı vardır -qipa [= <suboptions_list>] | -qnoipa İşlemler arası analiz (IPA) olarak bilinen bir optimizasyon sınıfını açar veya özelleştirir.
EvilTeach

4
En iyisi, bunu gerektirmeyen bir gelişme yoluna sahip olmaktır. Bu gerçeği modüler olmayan kod yazmak için bir bahane olarak kullanmak, genel olarak sadece yavaş ve bakım sorunları olan kodla sonuçlanacaktır.
Hogan

3
Sanırım bu bilgi biraz eski. Teoride, birçok derleyicide yerleşik olarak bulunan tüm program optimizasyon özellikleri (örneğin, gcc'de "Bağlantı Zamanı Optimizasyonu") aynı faydalara izin verir, ancak tamamen standart bir iş akışı ile (artı hepsini tek bir dosyaya koymaktan daha hızlı yeniden derleme süreleri) !)
Ponkadoodle

@Wallacoloo Elbette, bu çok eski bir tarih. FWIW, GCC'nin LTO'sunu bugün ilk kez kullandım ve - diğer her şey eşit olmak üzere -O3- programımın orijinal boyutunun% 22'sini patlattı. (CPU'ya bağlı değil, bu yüzden hız hakkında söyleyecek pek bir şeyim yok.)
undercore_d

4

Çoğu modern derleyici, kuyruk özyinelemesini hızlandırmak için iyi bir iş çıkarır, çünkü işlev çağrıları optimize edilebilir.

Misal:

int fac2(int x, int cur) {
  if (x == 1) return cur;
  return fac2(x - 1, cur * x); 
}
int fac(int x) {
  return fac2(x, 1);
}

Elbette bu örnekte herhangi bir sınır kontrolü yok.

Geç Düzenleme

Kod hakkında doğrudan bilgim olmasa da; CTE'leri SQL Server'da kullanma gereksinimlerinin, kuyruk uçlu özyineleme yoluyla optimize edebilecek şekilde özel olarak tasarlandığı açıktır.


1
soru C ile ilgilidir. C kuyruk özyinelemesini ortadan kaldırmaz, bu nedenle kuyruk veya başka özyineleme, özyineleme çok derine giderse yığın patlayabilir.
Kurbağa

1
Bir goto kullanarak arama kuralı sorununu önledim. Bu şekilde daha az masraf var.
EvilTeach

2
@hogan: Bu benim için yeni. Bunu yapan herhangi bir derleyiciye işaret edebilir misiniz? Ve onu gerçekten optimize ettiğinden nasıl emin olabilirsiniz? Eğer bunu yapacaksa, gerçekten yaptığından emin olmak gerekir. Bu, derleyici optimize edicinin öğrenmesini umduğunuz bir şey değil (işe yarayan veya çalışmayan satır içi gibi)
Toad

6
@hogan: Düzeltilmiş durumdayım. Gcc ve MSVC'nin her ikisinin de kuyruk özyineleme optimizasyonu yaptığı konusunda haklısınız.
Kurbağa

5
Bu örnek, son olan özyinelemeli çağrı değil, çarpma olduğu için kuyruk özyineleme değildir.
Brian Young

4

Aynı işi tekrar tekrar yapmayın!

Gördüğüm yaygın bir antipattern şu satırlardan geçiyor:

void Function()
{
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomething();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingElse();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingCool();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingReallyNeat();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingYetAgain();
}

Derleyici aslında tüm bu işlevleri her zaman çağırmak zorundadır. Varsayalım ki, programcı, bir araya getirilmiş nesnenin bu çağrılar sırasında değişmediğini bildiğini varsayarsak, kutsal olan her şeyin sevgisi için ...

void Function()
{
   MySingleton* s = MySingleton::GetInstance();
   AggregatedObject* ao = s->GetAggregatedObject();
   ao->DoSomething();
   ao->DoSomethingElse();
   ao->DoSomethingCool();
   ao->DoSomethingReallyNeat();
   ao->DoSomethingYetAgain();
}

Tekil alıcı durumunda, aramalar çok maliyetli olmayabilir, ancak kesinlikle bir maliyettir (tipik olarak, "nesnenin oluşturulup oluşturulmadığını kontrol edin, oluşturmadıysa, oluşturun ve sonra iade edin). Bu alıcılar zinciri ne kadar karmaşık hale gelirse, o kadar fazla boşa zamanımız olur.


3
  1. Tüm değişken bildirimleri için mümkün olan en yerel kapsamı kullanın.

  2. constMümkün olduğunca kullanın

  3. Dont kullanım kayıt sizinle ve onsuz hem profil düşünmüyorsanız

Bunlardan ilk 2'si, özellikle de 1 numara, optimize edicinin kodu analiz etmesine yardımcı olur. Hangi değişkenlerin kayıtlarda tutulacağı konusunda iyi seçimler yapmasına özellikle yardımcı olacaktır.

Register anahtar sözcüğünü körü körüne kullanmak, optimizasyonunuza zarar vermek kadar yardımcı olabilir. Montaj çıktısına veya profiline bakana kadar neyin önemli olacağını bilmek çok zor.

Koddan iyi performans elde etmek için önemli olan başka şeyler de var; örneğin önbellek tutarlılığını en üst düzeye çıkarmak için veri yapılarınızı tasarlamak. Ancak soru optimize edici ile ilgiliydi.



3

Bir keresinde karşılaştığım bir şey hatırlatıldı, burada semptom sadece hafızamızın tükenmesiydi, ancak sonuç önemli ölçüde artırılmış performans (ve hafıza ayak izinde büyük azalmalar).

Bu durumda sorun, kullandığımız yazılımın tonlarca küçük tahsisat yapmasıydı. Mesela, buraya dört bayt, orada altı bayt ayırmak gibi. 8-12 bayt aralığında çalışan birçok küçük nesne de. Sorun, programın pek çok küçük şeye ihtiyaç duyması değildi, çok sayıda küçük şeyi tek tek ayırmasıydı, bu da her tahsisatı (bu platformda) 32 bayta şişirdi.

Çözümün bir parçası, Alexandrescu tarzı bir küçük nesne havuzunu bir araya getirmek, ancak onu genişletmek, böylece küçük nesnelerin dizilerini ve tek tek öğeleri ayırabilmekti. Bu, performans açısından son derece yardımcı oldu, çünkü herhangi bir zamanda önbelleğe daha fazla öğe sığdı.

Çözümün diğer kısmı, manuel olarak yönetilen char * üyelerinin yaygın kullanımını bir SSO (küçük dizeli optimizasyon) dizesiyle değiştirmekti. Minimum ayırma 32 bayt olduğundan, bir char * arkasında gömülü 28 karakterlik bir arabelleğe sahip bir dize sınıfı oluşturdum, bu nedenle dizelerimizin% 95'inin ek bir ayırma yapmasına gerek kalmadı (ve sonra neredeyse her görünümünü manuel olarak değiştirdim. char * bu kütüphanede bu yeni sınıfla, bu eğlenceli olsun ya da olmasın). Bu, bellek parçalanmasında da bir ton yardımcı oldu, bu daha sonra diğer işaret edilen nesneler için referans yerelliğini artırdı ve benzer şekilde performans kazanımları oldu.


3

@ MSalters yorumundan öğrendiğim temiz bir teknik, bu cevaba göre derleyicilerin farklı nesneleri bazı koşullara göre döndürürken bile kopya eleme yapmasına izin veriyor:

// before
BigObject a, b;
if(condition)
  return a;
else
  return b;

// after
BigObject a, b;
if(condition)
  swap(a,b);
return a;

2

Tekrar tekrar çağırdığınız küçük işlevleriniz varsa, geçmişte bunları başlıklara "statik satır içi" olarak koyarak büyük kazançlar elde ettim. İx86'daki işlev çağrıları şaşırtıcı derecede pahalıdır.

Açık bir yığın kullanarak özyinelemeli olmayan bir şekilde özyinelemeli işlevleri yeniden uygulamak da çok şey kazanabilir, ancak o zaman gerçekten geliştirme zamanı ve kazanç dünyasındasınız.


Özyinelemeyi bir yığına dönüştürmek , ompf.org'da ışın izleyici geliştiren ve başka işleme algoritmaları yazan kişiler için varsayılan bir optimizasyondur.
Tom

... Buna, kişisel raytracer projemdeki en büyük ek yükün Bileşik desen kullanan bir sınırlayıcı hacim hiyerarşisi aracılığıyla vtable tabanlı özyineleme olduğunu eklemeliyim. Bu gerçekten sadece bir ağaç olarak yapılandırılmış bir grup iç içe geçmiş kutu, ancak modeli kullanmak veri şişmesine (sanal tablo işaretçileri) neden olur ve talimat tutarlılığını azaltır (küçük / sıkı bir döngü artık bir işlev çağrıları zinciridir)
Tom

2

İşte ikinci optimizasyon tavsiyem. İlk tavsiyemde olduğu gibi, bu genel amaçlıdır, dile veya işlemciye özgü değildir.

Derleyici kılavuzunu iyice okuyun ve size ne anlattığını anlayın. Derleyiciyi en üst düzeyde kullanın.

Bir programdan performansı düşürmek için doğru algoritmayı seçmenin kritik olduğunu belirleyen diğer katılımcılardan bir veya ikisine katılıyorum. Bunun ötesinde, derleyiciyi kullanmaya yatırım yaptığınız zamandaki getiri oranı (kod yürütme iyileştirmesinde ölçülür), kodu değiştirirken elde edilen getiri oranından çok daha yüksektir.

Evet, derleyici yazarları kodlama devlerinden değildir ve derleyiciler hatalar içerir ve el kitabına ve derleyici teorisine göre işleri hızlandıran şeyler bazen işleri yavaşlatır. Bu nedenle, her seferinde bir adım atmanız ve ince ayar öncesi ve sonrası performansı ölçmeniz gerekir.

Ve evet, nihayetinde, derleyici bayraklarının birleşimsel patlamasıyla karşılaşabilirsiniz, bu nedenle çeşitli derleyici bayraklarıyla make çalıştırmak, işleri büyük küme üzerinde sıraya koymak ve çalışma zamanı istatistiklerini toplamak için bir veya iki betiğinizin olması gerekir. Yalnızca siz ve bir PC'de Visual Studio varsa, yeterli derleyici bayraklarının yeterli kombinasyonunu denemeden çok önce ilginiz biter.

Saygılarımızla

işaret

Bir kod parçasını ilk aldığımda genellikle 1.4 - 2.0 kat daha fazla performans elde edebilirim (yani kodun yeni sürümü, eski sürümün 1 / 1.4 veya 1 / 2'si ile çalışır) derleyici bayraklarıyla uğraşarak iki gün. Kabul ediyorum, bu mükemmeliyetimin bir belirtisi olmaktan ziyade, üzerinde çalıştığım kodun çoğunu oluşturan bilim adamları arasındaki derleyici anlayışı eksikliğine dair bir yorum olabilir. Derleyici bayraklarını maksimuma (ve nadiren -O3) ayarladıktan sonra, başka bir 1.05 veya 1.1 çarpanı elde etmek için aylarca sıkı çalışma gerekebilir.


2

DEC alfa işlemcileriyle birlikte çıktığında, derleyici her zaman otomatik olarak kayıtlara 6 argüman koymaya çalışacağından, bir fonksiyona ait argüman sayısını 7'nin altında tutma önerisi vardı.


x86-64 bit ayrıca, işlev çağrısı ek yükü üzerinde dramatik bir etkiye sahip olabilen çok sayıda yazmaç geçirilmiş parametreye izin verir.
Tom

1

Performans için, öncelikle bakımı yapılabilir kod yazmaya odaklanın - bileşenli, gevşek bağlı, vb, böylece bir parçayı yeniden yazmak, optimize etmek veya basitçe profillemek için izole etmeniz gerektiğinde, çok fazla çaba harcamadan yapabilirsiniz.

Optimizer, programınızın performansına marjinal olarak yardımcı olacaktır.


3
Bu sadece kuplaj "arayüzlerinin" kendileri optimizasyona uygunsa çalışır. Arayüz, doğası gereği "yavaş" olabilir, örneğin gereksiz aramaları veya hesaplamaları zorlayarak veya kötü önbellek erişimini zorlayarak.
Tom

1

Burada iyi yanıtlar alıyorsunuz, ancak programınızın başlangıçta optimuma oldukça yakın olduğunu varsayıyorlar ve

Programın doğru yazıldığını, tam optimizasyon ile derlendiğini, test edildiğini ve üretime alındığını varsayalım.

Deneyimlerime göre, bir program doğru yazılabilir, ancak bu onun optimal olduğu anlamına gelmez. Bu noktaya gelmek ekstra çalışma gerektirir.

Bir örnek verebilirsem, bu cevap , mükemmel bir şekilde makul görünen bir programın makro optimizasyon ile 40 kat daha hızlı nasıl yapıldığını gösterir . İlk yazıldığı gibi her programda büyük hızlandırmalar yapılamaz , ancak birçok durumda (çok küçük programlar hariç), benim deneyimime göre olabilir.

Bu yapıldıktan sonra, mikro optimizasyon (etkin noktaların) size iyi bir getiri sağlayabilir.


1

intel derleyici kullanıyorum. hem Windows hem de Linux'ta.

az ya da çok bittiğinde kodun profilini çıkarırım. daha sonra sıcak noktalara asın ve derleyicinin daha iyi bir iş yapmasına izin vermek için kodu değiştirmeye çalışın.

eğer bir kod hesaplamalı bir kodsa ve çok sayıda döngü içeriyorsa - intel derleyicideki vektörleştirme raporu çok yararlıdır - yardımda 'vec-report'u arayın.

bu yüzden ana fikir - performans kritik kodunu cilalamak. geri kalanına gelince - doğru ve sürdürülebilir olma önceliği - kısa işlevler, 1 yıl sonra anlaşılabilecek açık kod.


Şu soruyu yanıtlamaya yaklaşıyorsunuz ..... derleyicinin bu tür optimizasyonları yapmasını mümkün kılmak için koda ne tür şeyler yapıyorsunuz?
EvilTeach

1
Daha çok C-stilinde yazmaya çalışmak (C ++ ile karşılaştırıldığında), örneğin mutlak ihtiyaç olmadan sanal işlevlerden kaçınmak, özellikle sık sık çağrılacaklarsa, AddRefs'den ve tüm harika şeylerden (yine gerçekten gerekmedikçe) kaçının. Satır içi için kod yazın - daha az parametre, daha az "if" -s. Mutlak ihtiyaç olmadıkça global değişkenler kullanmayın. Veri yapısında - daha geniş alanları ilk sıraya koyun (double, int64 int'den önce gelir) - böylece derleyici yapıyı ilk alan doğal boyutuna hizalayın - performans için iyi hizalama.
jf.

1
Veri düzeni ve erişim, performans için kesinlikle kritiktir. Bu yüzden profil oluşturduktan sonra - bazen bir yapıyı erişimlerin yerelliğini izleyen birkaç yapıya bölerim. Bir genel numara daha - int veya size-t ile char kullanın - veri değerleri küçük olsa bile - çeşitli performanslardan kaçının. cezalar, yükleme engelleme için depolar, kısmi kayıtlarla ilgili sorunlar durur. Tabii ki bu, bu türden büyük veri dizilerine ihtiyaç duyulduğunda uygulanamaz.
jf.

Bir tane daha - gerçekten ihtiyaç olmadıkça sistem çağrılarından kaçının :) - ÇOK pahalıdırlar
jf.

2
@jf: Cevabınıza +1 yaptım, ancak lütfen cevabı yorumlardan cevap gövdesine taşıyabilir misiniz? Okumak daha kolay olacak.
kriss

1

C ++ 'da kullandığım bir optimizasyon, hiçbir şey yapmayan bir kurucu yaratmaktır. Nesneyi çalışır duruma getirmek için bir init () 'i manuel olarak çağırmak gerekir.

Bu, bu sınıfların büyük bir vektöre ihtiyacım olduğu durumda faydalıdır.

Vektör için yer ayırmak için rezerv () çağırıyorum, ancak kurucu aslında nesnenin bulunduğu bellek sayfasına dokunmuyor. Bu yüzden biraz adres alanı harcadım ama aslında çok fazla fiziksel bellek tüketmedim. İlgili inşaat maliyetleriyle ilgili sayfa hatalarından kaçınırım.

Vektörü doldurmak için nesneler üretirken onları init () kullanarak ayarlarım. Bu, toplam sayfa hatalarımı sınırlar ve vektörü doldururken yeniden boyutlandırma () ihtiyacını ortadan kaldırır.


6
Tipik bir std :: vector gerçeklemesinin, daha fazla kapasite ayırdığınızda () daha fazla nesne oluşturmadığına inanıyorum. Sadece sayfaları ayırır. Yapıcılar daha sonra, yeni yerleşim kullanılarak, vektöre nesneler eklediğinizde (muhtemelen) init () 'i çağırmadan hemen önce çağrılır, bu nedenle gerçekten ayrı init () işlevine ihtiyacınız yoktur. Ayrıca, kurucunuz kaynak kodda "boş" olsa bile, derlenen kurucunun sanal tablolar ve RTTI gibi şeyleri başlatmak için kod içerebileceğini, böylece sayfalara her durumda inşaat sırasında dokunulabileceğini unutmayın.
Wyzard

1
Evet. Bizim durumumuzda vektörü doldurmak için push_back kullanırız. Nesnelerin herhangi bir sanal işlevi yoktur, bu nedenle bu bir problem değildir. Yapıcı ile ilk kez denediğimizde, sayfa hatalarının hacmi karşısında hayrete düştük. Ne olduğunu anladım ve kurucunun cesaretini aldık ve sayfa hatası sorunu ortadan kalktı.
EvilTeach

Bu beni oldukça şaşırtıyor. Hangi C ++ ve STL uygulamalarını kullanıyordunuz?
David Thornley

3
Diğerlerine katılıyorum, bu std :: vector'un kötü bir uygulaması gibi geliyor. Nesnelerinizin vtables'ı olsa bile, push_back'inize kadar inşa edilmezler. Bunu, varsayılan kurucuyu özel olarak bildirerek test edebilmelisiniz, çünkü tüm vektörün, push_back için kopya yapıcı olması gerekir.
Tom

1
@David - Uygulama AIX üzerindeydi.
EvilTeach

1

Yaptığım bir şey, pahalı eylemleri kullanıcının programın biraz gecikmesini bekleyebileceği yerlerde tutmaya çalışmaktı. Genel performans yanıt verebilirlikle ilgilidir, ancak tamamen aynı değildir ve birçok şey için yanıt verme, performansın daha önemli bir parçasıdır.

Genel performansta gerçekten iyileştirmeler yapmak zorunda kaldığım son sefer, optimumun altındaki algoritmalara dikkat ettim ve önbellek sorunları olması muhtemel yerleri aradım. Önce ve her değişiklikten sonra performansın profilini çıkardım ve ölçtüm. Sonra şirket çöktü ama yine de ilginç ve öğretici bir işti.


0

Uzun zamandır şüphelendim, ancak dizileri eleman sayısı olarak 2 kuvvetine sahip olacak şekilde ilan etmenin, optimize edicinin , yukarı bakarken bir kayma ile çarpmayı birkaç bit ile değiştirerek bir güç azaltımı yapmasını sağladığını asla kanıtlamadım. bireysel unsurlar.


6
Bu eskiden doğruydu, bugünlerde artık öyle. Aslında tam tersi doğrudur. Dizilerinizi ikinin gücüyle bildirirseniz, büyük olasılıkla bellekte iki ayrı güçte iki işaretçi üzerinde çalıştığınız durumla karşılaşacaksınız. Sorun şu ki, CPU önbellekleri aynı şekilde organize edilmiş ve sonunda iki dizinin bir önbellek hattı etrafında savaşması ile karşılaşabilirsiniz. Bu şekilde korkunç bir performans elde edersiniz. İşaretçilerden birinin birkaç bayt önde olması (örneğin ikinin kuvvetsiz olması) bu durumu engeller.
Nils Pipenbrinck

+1 Nils, ve bunun belirli bir örneği Intel donanımındaki "64k örtüşme" dir.
Tom

Bu arada, sökmeye bakıldığında kolayca çürütülen bir şey. Yıllar önce, gcc'nin vardiya ve eklemelerle her türlü sabit çarpımı nasıl optimize ettiğini görünce şaşırmıştım. Örneğin val * 7, aksi halde neye benzeyeceğine dönüştü (val << 3) - val.
dash-tom-bang

0

Küçük ve / veya sık çağrılan işlevleri kaynak dosyanın en üstüne koyun. Bu, derleyicinin satır içi oluşturma fırsatları bulmasını kolaylaştırır.


Gerçekten mi? Bunun için bir mantık ve örnekler verebilir misiniz? Doğru olmadığını söylemiyorum, sadece konumun önemli olacağı sezgisel değil.
underscore_d

@underscore_d, işlev tanımı bilinene kadar bir şeyi satır içi yapamaz. Modern derleyiciler, tanımın kod oluşturma zamanında bilinmesi için birden çok geçiş yapabilirken, ben bunu varsaymıyorum.
Mark Ransom

Derleyicilerin fiziksel fonksiyon sırasından ziyade soyut arama grafikleri ile çalıştığını varsaymıştım, yani bunun önemi yoktu. Elbette, fazladan dikkatli olmanın zararı olmadığını düşünüyorum - özellikle performans bir yana, IMO, onları çağıranlardan önce çağrılan işlevleri tanımlamak daha mantıklı görünüyor. Performansı test etmem gerekirdi ama önemli olursa şaşırırdım ama o zamana kadar şaşırmaya açığım!
underscore_d
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.