Sanal işlevler ve performans - C ++


125

Sınıf tasarımımda, soyut sınıfları ve sanal işlevleri yoğun bir şekilde kullanıyorum. Sanal işlevlerin performansı etkilediği hissine kapıldım. Bu doğru mu? Ancak bu performans farkının fark edilmediğini düşünüyorum ve erken optimizasyon yapıyorum gibi görünüyor. Sağ?


Cevabıma göre, bunu stackoverflow.com/questions/113830
Suma


2
Yüksek performanslı bilgi işlem ve sayı hesaplama yapıyorsanız, hesaplamanın özünde herhangi bir sanallık kullanmayın: kesinlikle tüm performansları öldürür ve derleme zamanında optimizasyonları önler. Programın başlatılması veya sonlandırılması için önemli değildir. Arayüzlerle çalışırken sanallığı dilediğiniz gibi kullanabilirsiniz.
Vincent

Yanıtlar:


90

İyi bir kural şudur:

Siz ispatlayana kadar bu bir performans sorunu değildir.

Sanal işlevlerin kullanımının performans üzerinde çok küçük bir etkisi olacaktır, ancak uygulamanızın genel performansını etkileme olasılığı düşüktür. Performans iyileştirmeleri aramak için daha iyi yerler, algoritmalarda ve G / Ç'dedir.

Sanal işlevlerden (ve daha fazlasından) bahseden mükemmel bir makale, Üye İşlev İşaretçileri ve Olası En Hızlı C ++ Delegeleridir .


Peki ya saf sanal işlevler? Performansı herhangi bir şekilde etkiliyorlar mı? Sadece uygulamayı zorlamak için orada olduklarını görünce merak ediyorum.
thomthom

2
@thomthom: Doğru, saf sanal ve sıradan sanal işlevler arasında performans farkı yok.
Greg Hewgill

168

Sorunuz beni meraklandırdı, bu yüzden çalıştığımız 3GHz sıralı PowerPC CPU'da bazı zamanlamalar yaptım. Yaptığım test, get / set işlevleriyle basit bir 4d vektör sınıfı yapmaktı

class TestVec 
{
    float x,y,z,w; 
public:
    float GetX() { return x; }
    float SetX(float to) { return x=to; }  // and so on for the other three 
}

Sonra her biri bu vektörlerden 1024'ünü içeren (L1'e sığacak kadar küçük) üç dizi kurdum ve bunları birbirine ekleyen bir döngü (Ax = Bx + Cx) 1000 kez çalıştırdım. Ben olarak tanımlanan fonksiyonları ile bu koştum inline, virtualve düzenli işlev çağrıları. Sonuçlar burada:

  • satır içi: 8ms (çağrı başına 0.65ns)
  • doğrudan: 68ms (arama başına 5.53ns)
  • sanal: 160ms (çağrı başına 13ns)

Dolayısıyla, bu durumda (her şeyin önbelleğe sığdığı) sanal işlev çağrıları, satır içi çağrılardan yaklaşık 20 kat daha yavaştı. Ama bu gerçekten ne anlama geliyor? Döngü boyunca yapılan her yolculuk, tam olarak 3 * 4 * 1024 = 12,288işlev çağrılarına (1024 vektör çarpı dört bileşen çarpı ekleme başına üç çağrı) neden olur, bu nedenle bu zamanlar 1000 * 12,288 = 12,288,000işlev çağrılarını temsil eder . Sanal döngü, doğrudan döngüden 92 ms daha uzun sürdü, bu nedenle çağrı başına ek yük, işlev başına 7 nanosaniye idi.

Bundan şu sonuca varıyorum: evet , sanal işlevler doğrudan işlevlerden çok daha yavaştır ve hayır , onları saniyede on milyon kez çağırmayı planlamıyorsanız, önemli değil.

Ayrıca bkz: oluşturulan montajın karşılaştırması.


Ancak birden çok kez aranırlarsa, genellikle yalnızca bir kez arandıklarından daha ucuz olabilirler. Alakasız bloguma bakın: phresnel.org/blog , "Zararlı olmadığı düşünülen sanal işlevler" başlıklı gönderiler, ama elbette kod yollarınızın karmaşıklığına bağlıdır
Sebastian Mach

22
Testim, tekrar tekrar çağrılan küçük bir sanal işlevler kümesini ölçer. Blog gönderiniz, kodun zaman maliyetinin işlemleri sayarak ölçülebileceğini varsayar, ancak bu her zaman doğru değildir; Modern işlemcilerde bir sanal işlevin en büyük maliyeti, bir dalın yanlış tahmininden kaynaklanan boru hattı balonudur.
Crashworks

10
bu, gcc LTO (Bağlantı Süresi Optimizasyonu) için harika bir ölçüt olacaktır; lto etkin: gcc.gnu.org/wiki/LinkTimeOptimization ile bunu tekrar derlemeyi deneyin ve 20x faktöründe ne olduğunu görün
lurscher

1
Bir sınıfın bir sanal ve bir satır içi işlevi varsa, sanal olmayan yöntemin performansı da etkilenecek mi? Sadece sınıfın sanal olması nedeniyle mi?
thomthom

4
@thomthom Hayır, sanal / sanal olmayan işlev başına bir özniteliktir. Bir işlev, yalnızca sanal olarak işaretlenmişse veya sanal olarak sahip olan bir temel sınıfı geçersiz kılıyorsa vtable aracılığıyla tanımlanmalıdır. Genel arayüz için bir grup sanal işleve sahip sınıfları ve ardından çok sayıda satır içi erişimci vb. (Teknik olarak, bu uygulamaya özgüdür ve bir derleyici, 'satır içi' olarak işaretlenmiş işlevler için bile sanal ponter kullanabilir, ancak böyle bir derleyici yazan kişi deli olur.)
Crashworks

42

Objective-C (tüm yöntemler sanaldır) iPhone için birincil dil olduğunda ve acayip Java Android için ana dil olduğunda, 3 GHz çift çekirdekli kulelerimizde C ++ sanal işlevleri kullanmanın oldukça güvenli olduğunu düşünüyorum.



13
@Crashworks: iPhone hiç bir kod örneği değil. Bu bir donanım örneğidir - özellikle yavaş donanım , burada değindiğim nokta bu. Bu "yavaş" diller, güçsüz donanım için yeterince iyiyse, sanal işlevler çok büyük bir sorun olmayacak.
Chuck

52
İPhone bir ARM işlemcide çalışıyor. İOS için kullanılan ARM işlemciler, düşük MHz ve düşük güç kullanımı için tasarlanmıştır. CPU'da dal tahmini için silikon yoktur ve bu nedenle, sanal işlev çağrılarında şube tahmininden kaynaklanan performans ek yükü kaçırmaz. Ayrıca iOS donanımı için MHz, RAM'den veri alırken bir önbelleğin kaybolması işlemciyi 300 saat döngüsü boyunca durdurmayacak kadar düşüktür. Düşük MHz'de önbellek atlamaları daha az önemlidir. Kısacası, iOS cihazlarda sanal işlevlerin kullanılmasının ek yükü yoktur, ancak bu bir donanım sorunudur ve masaüstü CPU'ları için geçerli değildir.
HaltingState

4
C ++ 'ya yeni başlayan uzun süredir Java programcısı olarak, Java'nın JIT derleyicisinin ve çalışma zamanı optimize edicisinin, önceden tanımlanmış bir döngü sayısından sonra çalışma zamanında bazı işlevleri derleme, tahmin etme ve hatta satır içi becerisine sahip olduğunu eklemek istiyorum. Bununla birlikte, C ++ 'nın derleme ve bağlantı sırasında böyle bir özelliğe sahip olup olmadığından emin değilim, çünkü çalışma zamanı çağrı kalıbı eksik. Bu nedenle C ++ 'da biraz daha dikkatli olmamız gerekebilir.
Alex Suo

@AlexSuo Ne demek istediğinden emin değilim? Derlendiğinde, C ++ elbette çalışma zamanında olabileceklere göre optimize edemez, bu nedenle tahmin vb. CPU'nun kendisi tarafından yapılmalıdır ... ancak iyi C ++ derleyicileri (talimat verilirse) çok daha önce işlevleri ve döngüleri optimize etmek için büyük çaba sarf eder Çalışma süresi.
alt

34

Çok performans açısından kritik uygulamalarda (video oyunları gibi) sanal bir işlev çağrısı çok yavaş olabilir. Modern donanımda, en büyük performans sorunu önbellek eksikliğidir. Veriler önbellekte değilse, mevcut olması yüzlerce döngü olabilir.

Normal bir işlev çağrısı, CPU yeni işlevin ilk talimatını aldığında ve önbellekte bulunmadığında bir talimat önbelleği eksikliğine neden olabilir.

Bir sanal işlev çağrısının önce nesneden vtable işaretçisini yüklemesi gerekir. Bu, veri önbelleğinin kaybolmasına neden olabilir. Ardından, işlev işaretçisini vtable'dan yükler ve bu da başka bir veri önbelleğinin kaybolmasına neden olabilir. Ardından, sanal olmayan bir işlev gibi bir talimat önbelleğinin kaybolmasına neden olabilecek işlevi çağırır.

Çoğu durumda, fazladan iki önbellek kaçırma sorun değildir, ancak kritik performans kodundaki sıkı döngüde performansı önemli ölçüde düşürebilir.


6
Doğru, ancak sıkı bir döngüden tekrar tekrar çağrılan herhangi bir kod (veya vtable) (elbette) nadiren önbellekte eksiklik yaşayacaktır. Ayrıca, vtable işaretçisi genellikle çağrılan yöntemin erişeceği nesnedeki diğer verilerle aynı önbellek satırındadır, bu nedenle genellikle yalnızca bir ekstra önbellek eksikliğinden bahsediyoruz.
Qwertie

5
@Qwertie Bunun gerekli olduğunu düşünmüyorum doğru. Döngünün gövdesi (L1 önbelleğinden büyükse) vtable işaretçisini, işlev işaretçisini "kullanımdan kaldırabilir" ve sonraki yineleme, her yinelemede L2 önbellek (veya daha fazla) erişimini beklemek zorunda kalır
Ghita

30

Agner Fog'un "C ++ Yazılımını Optimize Etme" kılavuzunun 44. sayfasından :

Bir sanal üye işlevi çağırmak için geçen süre, işlev çağrısı deyiminin her zaman sanal işlevin aynı sürümünü çağırması koşuluyla, sanal olmayan bir üye işlevi çağırmak için gerekenden birkaç saat döngüsü fazladır. Sürüm değişirse, 10 - 30 saat döngüsü yanlış tahmin cezası alırsınız. Sanal işlev çağrılarının tahmini ve yanlış tahminine ilişkin kurallar, anahtar ifadeleriyle aynıdır ...


Bu referans için teşekkürler. Agner Fog'un optimizasyon kılavuzları, donanımı en iyi şekilde kullanmak için altın standarttır.
Arto Bendiken

Hatırladığım ve hızlı aramaya dayanarak - stackoverflow.com/questions/17061967/c-switch-and-jump-tables - bunun her zaman doğru olduğundan şüpheliyim switch. Tamamen keyfi casedeğerlerle, elbette. Ama eğer tüm cases ardışıksa, bir derleyici bunu bir atlama tablosuna (ah, bu bana eski güzel Z80 günlerini hatırlatıyor), yani (daha iyi bir terim istemek için) sabit zaman olarak optimize edebilir. Değil ben vfuncs değiştirmeye çalışırken öneririz switchgülünç olan. ;)
alt

7

kesinlikle. Her yöntem çağrısı çağrılmadan önce vtable'da bir arama gerektirdiğinden, bilgisayarlar 100Mhz'de çalıştığında bu bir problemdi. Ama bugün .. ilk bilgisayarımdan daha fazla belleğe sahip 1. seviye önbelleğe sahip bir 3Ghz CPU'da? Bir şey değil. Ana RAM'den bellek ayırmak, tüm işlevlerinizin sanal olmasına kıyasla size daha fazla zamana mal olacaktır.

Bu, insanların yapılandırılmış programlamanın yavaş olduğunu söylediği eski günlerdeki gibi, çünkü tüm kod işlevlere bölündü, her işlev yığın tahsisleri ve bir işlev çağrısı gerektiriyordu!

Bir sanal işlevin performans etkisini düşünmeyi düşündüğüm tek zaman, çok yoğun bir şekilde kullanılmış ve her şey boyunca sona eren şablonlu kodda somutlaştırılmışsa. O zaman bile, bunun için çok fazla çaba sarf etmem!

PS, diğer 'kullanımı kolay' dilleri düşünür - tüm yöntemleri kapakların altında sanaldır ve günümüzde taranmazlar.


4
Bugün bile işlev çağrılarından kaçınmak, yüksek performanslı uygulamalar için önemlidir. Aradaki fark, bugünün derleyicileri küçük işlevleri güvenilir bir şekilde satır içi yapar, bu nedenle küçük işlevler yazarken hız cezalarına maruz kalmayız. Sanal işlevlere gelince, akıllı CPU'lar üzerlerinde akıllı dal tahmini yapabilir. Eski bilgisayarların daha yavaş olması gerçeği, bence, aslında sorun değil - evet, çok daha yavaşlardı, ama o zamanlar bunu biliyorduk, bu yüzden onlara çok daha küçük iş yükleri verdik. 1992'de bir MP3 çalsaydık, işlemcinin yarısından fazlasını bu göreve adamamız gerekebileceğini biliyorduk.
Qwertie

6
mp3, 1995'ten kalmadır. 92'de neredeyse 386'mız vardı, bir mp3 çalmalarına imkân yoktu ve cpu zamanının% 50'si iyi bir çoklu görev işletim sistemi, boşta bir işlem ve önleyici bir zamanlayıcı varsayıyor. O zamanlar tüketici pazarında bunların hiçbiri yoktu. gücün AÇIK olduğu andan itibaren öykünün sonunda% 100'tü.
v.oddou

7

Yürütme süresinin yanı sıra başka bir performans kriteri var. Bir Vtable bellek alanını da kaplar ve bazı durumlarda önlenebilir: ATL, şablonlarla derleme zamanı " simüle edilmiş dinamik bağlama " kullanıraçıklaması zor olan "statik polimorfizm" etkisini elde etmek; temelde türetilmiş sınıfı bir temel sınıf şablonuna bir parametre olarak geçirirsiniz, bu nedenle derleme zamanında temel sınıf her örnekte türetilmiş sınıfının ne olduğunu "bilir". Bir temel türler koleksiyonunda (bu çalışma zamanı polimorfizmidir) birden çok farklı türetilmiş sınıfı depolamanıza izin vermez, ancak statik anlamda, önceden var olan bir X şablon sınıfıyla aynı olan bir Y sınıfı yapmak istiyorsanız, Bu tür bir geçersiz kılma için hooks, sadece önem verdiğiniz yöntemleri geçersiz kılmanız gerekir ve sonra bir vtable'a sahip olmadan X sınıfının temel yöntemlerini elde edersiniz.

Büyük bellek ayak izlerine sahip sınıflarda, tek bir vtable işaretçisinin maliyeti çok fazla değildir, ancak COM'daki bazı ATL sınıfları çok küçüktür ve çalışma zamanı polimorfizm durumu asla gerçekleşmeyecekse vtable tasarrufuna değer.

Ayrıca bu diğer GK sorusuna bakın .

Bu arada, burada CPU zamanı performans yönlerinden bahseden bir gönderi buldum .



4

Evet, haklısınız ve sanal işlev çağrısının maliyetini merak ediyorsanız, bu yazıyı ilginç bulabilirsiniz .


1
Bağlantılı makale, sanal aramanın çok önemli bir bölümünü dikkate almıyor ve bu olası şube yanlış tahminidir.
Suma

4

Bir sanal işlevin bir performans sorunu haline geleceğini görebilmemin tek yolu, çok sayıda sanal işlevin sıkı bir döngü içinde çağrılması ve ancak ve ancak bir sayfa hatasına veya diğer "ağır" bellek işlemlerinin gerçekleşmesine neden olmalarıdır.

Diğer insanların söylediği gibi, gerçek hayatta sizin için hemen hemen hiçbir zaman sorun olmayacak. Ve eğer öyle olduğunu düşünüyorsanız, bir profil oluşturucu çalıştırın, bazı testler yapın ve bir performans avantajı için kodunuzun "tasarımını geri almayı" denemeden önce bunun gerçekten bir sorun olup olmadığını kontrol edin.


2
Sıkı bir döngüdeki herhangi bir şeyi aramak büyük olasılıkla tüm bu kodu ve verileri önbellekte sıcak tutacaktır ...
Greg Rogers

2
Evet, ancak bu sağ döngü bir nesne listesi boyunca yineleniyorsa, o zaman her nesne potansiyel olarak aynı işlev çağrısı yoluyla farklı bir adreste sanal bir işlevi çağırıyor olabilir.
Daemin

3

Sınıf yöntemi sanal olmadığında, derleyici genellikle satır içi yapar. Aksine, sanal işlevi olan bir sınıfa işaretçi kullandığınızda, gerçek adres yalnızca çalışma zamanında bilinecektir.

Bu, test ile iyi bir şekilde gösterilmiştir, zaman farkı ~% 700 (!):

#include <time.h>

class Direct
{
public:
    int Perform(int &ia) { return ++ia; }
};

class AbstrBase
{
public:
    virtual int Perform(int &ia)=0;
};

class Derived: public AbstrBase
{
public:
    virtual int Perform(int &ia) { return ++ia; }
};


int main(int argc, char* argv[])
{
    Direct *pdir, dir;
    pdir = &dir;

    int ia=0;
    double start = clock();
    while( pdir->Perform(ia) );
    double end = clock();
    printf( "Direct %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );

    Derived drv;
    AbstrBase *ab = &drv;

    ia=0;
    start = clock();
    while( ab->Perform(ia) );
    end = clock();
    printf( "Virtual: %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );

    return 0;
}

Sanal işlev çağrısının etkisi büyük ölçüde duruma bağlıdır. Çok az çağrı varsa ve işlev içinde önemli miktarda çalışma varsa - bu önemsiz olabilir.

Ya da basit bir işlem yapılırken defalarca tekrar tekrar kullanılan sanal bir arama olduğunda - gerçekten büyük olabilir.


4
Sanal işlev çağrısı ile karşılaştırıldığında pahalıdır ++ia. Ne olmuş yani?
Bo Persson

2

Özel projemde en az 20 kez bu konuda ileri geri gittim. Orada rağmen edebilir kod yeniden, açıklık, idame ve okunabilirliği açısından bazı büyük kazançlar olması, diğer taraftan, performans isabet hala do sanal fonksiyonları ile mevcuttur.

Performans artışı modern bir dizüstü bilgisayarda / masaüstü / tablette fark edilecek mi ... muhtemelen değil! Bununla birlikte, gömülü sistemlerin olduğu bazı durumlarda, performans düşüşü kodunuzun verimsizliğinde itici faktör olabilir, özellikle sanal işlev bir döngü içinde tekrar tekrar çağrılırsa.

Gömülü sistemler bağlamında C / C ++ için en iyi uygulamaları analiz eden bazı tarihli bir makale: http://www.open-std.org/jtc1/sc22/wg21/docs/ESC_Boston_01_304_paper.pdf

Sonuç olarak: belirli bir yapıyı diğerine göre kullanmanın artılarını / eksilerini anlamak programcıya kalmıştır. Süper performans odaklı olmadığınız sürece, muhtemelen performans vuruşunu önemsemiyorsunuzdur ve kodunuzu olabildiğince kullanılabilir hale getirmeye yardımcı olmak için C ++ 'daki tüm düzgün OO öğelerini kullanmalısınız.


2

Tecrübelerime göre, en önemli şey, bir işlevi satır içi yapma becerisidir. Bir işlevin satır içine alınması gerektiğini dikte eden performans / optimizasyon ihtiyaçlarınız varsa, o zaman işlevi sanal yapamazsınız çünkü bunu engelleyecektir. Aksi takdirde, muhtemelen farkı fark etmeyeceksiniz.


1

Unutulmaması gereken bir şey şudur:

boolean contains(A element) {
    for (A current: this)
        if (element.equals(current))
            return true;
    return false;
}

bundan daha hızlı olabilir:

boolean contains(A element) {
    for (A current: this)
        if (current.equals(equals))
            return true;
    return false;
}

Bunun nedeni, birinci yöntemin yalnızca bir işlevi çağırması, ikincisinin ise birçok farklı işlevi çağırması olabilir. Bu, herhangi bir dildeki herhangi bir sanal işlev için geçerlidir.

"Olabilir" diyorum çünkü bu derleyiciye, önbelleğe vb. Bağlıdır.


0

Sanal işlevleri kullanmanın performans cezası, tasarım düzeyinde elde ettiğiniz avantajları asla geride bırakamaz. Bir sanal işleve yapılan çağrı, statik bir işleve doğrudan çağrıdan% 25 daha az verimli olacaktır. Bunun nedeni, VMT üzerinden bir yönlendirme seviyesi olmasıdır. Bununla birlikte, aramayı yapmak için geçen süre, işlevinizin fiili yürütülmesi sırasında geçen süreye kıyasla normalde çok küçüktür, bu nedenle toplam performans maliyeti, özellikle mevcut donanım performansıyla birlikte çok düşük olacaktır. Dahası, derleyici bazen optimize edebilir ve sanal çağrının gerekmediğini görebilir ve bunu statik bir çağrı olarak derleyebilir. Bu yüzden endişelenmeyin, sanal işlevleri ve soyut sınıfları ihtiyacınız olduğu kadar kullanın.


2
asla asla, hedef bilgisayar ne kadar küçük olursa olsun?
zumalifeguard

The performance penalty of using virtual functions can sometimes be so insignificant that it is completely outweighed by the advantages you get at the design level.Anahtar farkın söylediği gibi ifade etmiş olsaydın, hemfikir olabilirdim sometimes, değil never.
underscore_d

-1

Kendimi her zaman sorguladım, özellikle de - birkaç yıl önce - standart bir üye yöntemi çağrısının zamanlamalarını sanal bir yöntemle karşılaştırarak böyle bir test yaptım ve o sırada sonuçlara gerçekten kızgındım, boş sanal aramalar var. Sanal olmayanlardan 8 kat daha yavaş.

Bugün tampon sınıfımda, çok performans açısından kritik bir uygulamada daha fazla bellek ayırmak için sanal bir işlev kullanıp kullanmamaya karar vermek zorunda kaldım, bu yüzden Google'da arama yaptım (ve seni buldum) ve sonunda testi tekrar yaptım.

// g++ -std=c++0x -o perf perf.cpp -lrt
#include <typeinfo>    // typeid
#include <cstdio>      // printf
#include <cstdlib>     // atoll
#include <ctime>       // clock_gettime

struct Virtual { virtual int call() { return 42; } }; 
struct Inline { inline int call() { return 42; } }; 
struct Normal { int call(); };
int Normal::call() { return 42; }

template<typename T>
void test(unsigned long long count) {
    std::printf("Timing function calls of '%s' %llu times ...\n", typeid(T).name(), count);

    timespec t0, t1;
    clock_gettime(CLOCK_REALTIME, &t0);

    T test;
    while (count--) test.call();

    clock_gettime(CLOCK_REALTIME, &t1);
    t1.tv_sec -= t0.tv_sec;
    t1.tv_nsec = t1.tv_nsec > t0.tv_nsec
        ? t1.tv_nsec - t0.tv_nsec
        : 1000000000lu - t0.tv_nsec;

    std::printf(" -- result: %d sec %ld nsec\n", t1.tv_sec, t1.tv_nsec);
}

template<typename T, typename Ua, typename... Un>
void test(unsigned long long count) {
    test<T>(count);
    test<Ua, Un...>(count);
}

int main(int argc, const char* argv[]) {
    test<Inline, Normal, Virtual>(argc == 2 ? atoll(argv[1]) : 10000000000llu);
    return 0;
}

Ve bunun - aslında - artık gerçekten önemli olmamasına gerçekten şaşırdım. Satır içi satırların sanal olmayanlardan daha hızlı olması ve sanallardan daha hızlı olmaları mantıklı olsa da, önbelleğinizin gerekli verilere sahip olup olmadığına bakılmaksızın ve optimizasyon yapabilirsiniz. önbellek düzeyinde, bence bu, uygulama geliştiricilerinden çok derleyici geliştiricileri tarafından yapılmalıdır.


12
Sanırım derleyiciniz, kodunuzdaki sanal işlev çağrısının yalnızca Virtual :: call çağrısı yapabileceğini söyleyebilir. Bu durumda, onu sadece satır içi yapabilir. Siz istemeseniz bile derleyicinin Normal :: call'ı satır içine almasını engelleyen hiçbir şey yoktur. Bu nedenle, 3 işlem için aynı süreleri almanızın oldukça olası olduğunu düşünüyorum çünkü derleyici onlar için aynı kodu üretiyor.
Bjarke H. Roune
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.