std :: işlev vs şablon


161

C ++ 11 std::functionsayesinde functor sarmalayıcıları ailesini aldık . Ne yazık ki, bu yeni eklemeler hakkında sadece kötü şeyler duymaya devam ediyorum. En popüler olanları korkunç derecede yavaş olmalarıdır. Test ettim ve şablonlarla karşılaştırıldığında gerçekten emiyorlar.

#include <iostream>
#include <functional>
#include <string>
#include <chrono>

template <typename F>
float calc1(F f) { return -1.0f * f(3.3f) + 666.0f; }

float calc2(std::function<float(float)> f) { return -1.0f * f(3.3f) + 666.0f; }

int main() {
    using namespace std::chrono;

    const auto tp1 = system_clock::now();
    for (int i = 0; i < 1e8; ++i) {
        calc1([](float arg){ return arg * 0.5f; });
    }
    const auto tp2 = high_resolution_clock::now();

    const auto d = duration_cast<milliseconds>(tp2 - tp1);  
    std::cout << d.count() << std::endl;
    return 0;
}

111 ms vs 1241 ms. Bunun functionsanal aramalar yoluyla içleri örterken şablonların güzel bir şekilde eğimli olabileceğini varsayıyorum .

Açıkça görüldüğü gibi şablonların sorunları var:

  • kitaplıklar kapalı bir kod olarak yayınlanırken yapmak istemeyeceğiniz bir şey olmayan başlık olarak sağlanmalı,
  • benzer extern templatepolitika getirilmediği sürece derleme süresini çok daha uzatabilirler,
  • bir şablonun gereksinimlerini (kavramları, herhangi birini?) temsil etmenin temiz bir yolu yoktur (en azından benim için bilinir), ne tür bir fonksiyonun beklendiğini açıklayan bir yorum.

Böylelikle functions'nin geçen functorların fiili standardı olarak kullanılabileceğini ve yüksek performans beklenen şablonların kullanılması gereken yerlerde olduğunu varsayabilir miyim ?


Düzenle:

Benim derleyici Visual Studio 2012'dir olmadan CTP.


16
std::functionYalnızca gerçekten de çağrılabilir nesnelerin heterojen bir koleksiyonuna ihtiyacınız varsa kullanın (yani çalışma zamanında başka hiçbir ayrımcı bilgi mevcut değildir).
Kerrek SB

30
Yanlış şeyleri karşılaştırıyorsunuz. Şablonlar her iki durumda da kullanılır - " std::functionveya şablonlar" değildir. Burada sorun sadece bir lambda std::functionsarma vs bir lambda sarma değil düşünüyorum std::function. Şu anda sorunuz "Bir elma mı yoksa bir kase mi tercih etmeliyim?"
Orbit'te

7
İster 1ns ister 10ns olsun, ikisi de hiçbir şey değildir.
ipc

23
@ ipc:% 1000 hiçbir şey değil. OP'nin tanımladığı gibi, herhangi bir pratik amaç için ölçeklenebilirlik söz konusu olduğunda ilgilenmeye başlarsınız.
Orbit'te Hafiflik Yarışları

18
@ ipc 10 kat daha yavaş, çok büyük. Hızın taban çizgisiyle karşılaştırılması gerekir; sadece nanosaniye olduğu için önemli olmadığını düşünmek aldatıcı.
Paul Manta

Yanıtlar:


170

Genel olarak, size bir seçenek sunan tasarım durumuyla , şablonları kullanın . Tasarım kelimesini vurguladım, çünkü odaklanmanız gereken şey, kullanım durumları std::functionve şablonlar arasındaki farktır, ki bunlar oldukça farklıdır.

Genel olarak, şablon seçimi yalnızca daha geniş bir ilkenin örneğidir: derleme zamanında mümkün olduğunca çok kısıtlama belirtmeye çalışın . Gerekçe basittir: programınız oluşturulmadan önce bile bir hata veya tür uyumsuzluğu yakalayabilirseniz, müşterinize buggy programı göndermezsiniz.

Ayrıca, doğru bir şekilde işaret ettiğiniz gibi, şablon işlevlerine yapılan çağrılar statik olarak çözülür (yani derleme zamanında), bu nedenle derleyici kodu optimize etmek ve muhtemelen satır içi yapmak için gerekli tüm bilgilere sahiptir (çağrı bir vtable).

Evet, şablon desteğinin mükemmel olmadığı ve C ++ 11'in hala kavramlar için bir desteği olmadığı doğrudur; ancak std::functionsizi bu açıdan nasıl kurtaracağını göremiyorum . std::functionşablonlara bir alternatif değil, şablonların kullanılamadığı tasarım durumları için bir araçtır.

Bu tür bir kullanım durumu , belirli bir imzayla yapışan, ancak derleme zamanında somut türü bilinmeyen, çağrılabilir bir nesneyi çağırarak bir çağrıyı çalışma zamanında çözmeniz gerektiğinde ortaya çıkar . Bu genellikle potansiyel olarak farklı türlerde geri arama koleksiyonunuz olduğunda geçerlidir. , ancak tek olarak çağırmanız gereken bir ; kayıtlı geri aramaların türü ve sayısı, programınızın durumuna ve uygulama mantığına bağlı olarak çalışma zamanında belirlenir. Bu geri çağrıların bazıları functor olabilir, bazıları düz işlevler olabilir, bazıları ise diğer işlevlerin belirli argümanlara bağlanmasının bir sonucu olabilir.

std::functionve std::bindayrıca, fonksiyonların nesne olarak ele alındığı ve diğer fonksiyonların üretilmesi için doğal olarak kıvrılıp birleştirildiği C ++ 'da fonksiyonel programlamanın . Bu tür bir kombinasyon şablonlarla da elde edilebilse de, benzer bir tasarım durumu normal olarak çalışma zamanında birleştirilmiş çağrılabilir nesnelerin türünü belirlemeyi gerektiren kullanım durumlarıyla birlikte gelir.

Son olarak, std::functionkaçınılmaz olan başka durumlar da vardır , örneğin yazmak istiyorsanız özyinelemeli lambdalar ; ancak bu kısıtlamalar, inandığım kavramsal ayrımlardan daha fazla teknolojik sınırlamalarla belirlenir.

Özetlemek gerekirse , tasarıma odaklanın ve bu iki yapı için kavramsal kullanım durumlarının neler olduğunu anlamaya çalışın. Onları yaptığınız gibi karşılaştırırsanız, muhtemelen ait olmadıkları bir arenaya zorlarsınız.


23
Bence "Bu genellikle potansiyel olarak farklı türlerde, ancak aynı şekilde çağırmanız gereken bir geri arama koleksiyonunuz olduğunda geçerlidir;" önemli olan. Temel kuralım: " std::functionDepolama sonunda ve Funarayüzde şablon tercih et ".
R. Martinho Fernandes

2
Not: Beton tiplerini gizleme tekniğine tip silme denir (yönetilen dillerde tip silme ile karıştırılmamalıdır). Genellikle dinamik polimorfizm açısından uygulanır, ancak daha güçlüdür (örneğin unique_ptr<void>, sanal yıkıcılar olmayan türler için bile uygun yıkıcılar çağırmak).
ecatmur

2
@ecatmur: Terminoloji konusunda biraz huzursuz olmamıza rağmen maddeye katılıyorum. Dinamik polimorfizm benim için "derleme zamanında farklı formlar varsaymak" olarak yorumladığım statik polimorfizmin aksine "çalışma zamanında farklı formlar varsaymak" anlamına gelir; ikincisi şablonlarla elde edilemez. Benim için, tür silme, tasarım açısından, dinamik polimorfizme hiç ulaşamamanın bir tür önkoşuludur: farklı türdeki nesnelerle etkileşimde bulunmak için bazı düzgün bir arayüze ihtiyacınız vardır ve tür silme, türü soyutlamak için bir yoldur. özel bilgi.
Andy Prowl

2
@ecatmur: Dolayısıyla dinamik polimorfizm kavramsal örüntüdür, tip silme ise bunu gerçekleştirmeye izin veren bir tekniktir.
Andy Prowl

2
@Downvoter: Bu cevapta neyi yanlış bulduğunuzu duymak isterdim.
Andy Prowl

89

Andy Prowl, tasarım sorunlarını güzel bir şekilde ele aldı. Bu elbette çok önemli, ama asıl sorunun daha fazla performans sorunuyla ilgili olduğuna inanıyorum std::function.

Her şeyden önce, ölçüm tekniği hakkında kısa bir açıklama: Elde edilen 11 ms'nin calc1hiçbir anlamı yoktur. Gerçekten de, oluşturulan düzene bakıldığında (veya montaj kodunda hata ayıklama), VS2012'nin optimize edicisinin, çağrı sonucunun calc1yinelemeden bağımsız olduğunu ve çağrıyı döngüden çıkardığını fark edecek kadar zeki olduğunu görebilirsiniz :

for (int i = 0; i < 1e8; ++i) {
}
calc1([](float arg){ return arg * 0.5f; });

Ayrıca, calc1 görünür bir etkisi olmadığını ve çağrıyı tamamen bırakır. Bu nedenle, 111 ms, boş halkanın çalışması için geçen zamandır. (İyileştiricinin döngüyü koruduğuna şaşırdım.) Bu nedenle, döngülerdeki zaman ölçümlerine dikkat edin. Bu göründüğü kadar basit değil.

Belirtildiği gibi, optimize edicinin anlamak için daha fazla sorunu vardır std::functionve çağrıyı döngüden çıkarmaz. Yani 1241 ms için adil bir ölçüm calc2.

std::functionFarklı türdeki çağrılabilir nesneleri saklayabildiğine dikkat edin . Bu nedenle, depolama için bir tür silme sihri gerçekleştirmelidir. Genel olarak, bu dinamik bir bellek ayırma anlamına gelir (varsayılan olarak bir çağrı yoluyla new). Bunun oldukça maliyetli bir işlem olduğu iyi bilinmektedir.

Standart (20.8.11.2.1 / 5), VS2012'nin (özellikle orijinal kod için) yaptığı küçük nesneler için dinamik bellek tahsisini önlemek için uygulamaları kodlar.

Bellek ayırma işlemi söz konusu olduğunda ne kadar yavaşlayabileceği hakkında bir fikir edinmek için lambda ifadesini üç saniyeyi yakalamak üzere değiştirdim float. Bu, çağrılabilir nesneyi küçük nesne optimizasyonunu uygulamak için çok büyük hale getirir:

float a, b, c; // never mind the values
// ...
calc2([a,b,c](float arg){ return arg * 0.5f; });

Bu sürüm için süre yaklaşık 16000 ms'dir (orijinal kod için 1241 ms'ye kıyasla).

Son olarak, lambda'nın ömrünün std::function. Bu durumda, lambda'nın bir kopyasını saklamak yerine, std::functionona bir "referans" saklayabilir. "Referans" ile kastedilen std::reference_wrapper, fonksiyonlar std::refve tarafından kolayca oluşturulabilen bir std::cref. Daha doğrusu aşağıdakileri kullanarak:

auto func = [a,b,c](float arg){ return arg * 0.5f; };
calc2(std::cref(func));

süre yaklaşık 1860 ms'ye düşer.

Bunu bir süre önce yazdım:

http://www.drdobbs.com/cpp/efficient-use-of-lambda-expressions-and/232500059

Makalede söylediğim gibi, argümanlar C ++ 11'e zayıf desteği nedeniyle VS2010 için geçerli değildir. Yazma sırasında, sadece VS2012'nin bir beta sürümü mevcuttu, ancak C ++ 11 için desteği zaten bu konuda yeterliydi.


Gerçekten ilginç buluyorum, derleyici tarafından optimize edilen oyuncak örneklerini kullanarak herhangi bir yan etkisi olmadığı için kod hızını kanıtlamak istiyoruz. Birinin gerçek / üretim kodu olmadan bu tür ölçümlere nadiren bahis yapabileceğini söyleyebilirim.
Ghita

@ Ghita: Bu örnekte, kodun optimize edilmesini önlemek için önceki yinelemenin sonucu calc1olabilecek bir floatargüman alabilir . Gibi bir şey x = calc1(x, [](float arg){ return arg * 0.5f; });. Ayrıca, bu calc1kullanımları sağlamalıyız x. Ama bu henüz yeterli değil. Bir yan etki yaratmamız gerekiyor. Örneğin, ölçümden sonra x, ekrana yazdırma . Yine de, timimg ölçümleri için oyuncak kodları kullanmanın, gerçek / üretim koduyla ne olacağı konusunda her zaman mükemmel bir gösterge veremeyeceğini kabul ediyorum.
Cassio Neri

Bana öyle geliyor ki, ölçüt döngü içinde std :: function nesnesini oluşturuyor ve döngüde calc2'yi çağırıyor. Derleyicinin bunu optimize edip etmediğine bakılmaksızın (ve yapıcı bir vptr depolamak kadar basit olabilir), işlevin bir kez oluşturulduğu ve çağıran başka bir işleve geçtiği bir durumla daha fazla ilgilenirim bir döngü içinde. Yani inşa süresi yerine çağrı yükü (ve calc2 değil 'f' çağrısı). Ayrıca, f'yi bir döngüden ziyade bir döngüde (calc2'de) çağırmanın herhangi bir kaldırma işleminden fayda sağlaması da ilgilenecektir.
greggo

Mükemmel cevap. 2 şey: std::reference_wrapper(şablonları zorlamak için geçerli bir kullanım için güzel bir örnek ; sadece genel depolama için değil) ve VS'nin optimize edicisinin boş bir döngüyü atmakta başarısız olduğunu görmek komik ... Bu GCC hatasıylavolatile fark ettiğim gibi .
underscore_d

37

Clang ile ikisi arasında performans farkı yok

Clang (3.2, trunk 166872) (Linux'ta -O2) kullanarak, iki durumdaki ikili dosyalar aslında aynıdır .

- Gönderinin sonunda klanlara geri döneceğim. Ama önce, gcc 4.7.2:

Halihazırda çok fazla fikir var, ancak calc1 ve calc2 hesaplamalarının sonuçlarının astar içi vb. Nedeniyle aynı olmadığını belirtmek istiyorum. Örneğin tüm sonuçların toplamını karşılaştırın:

float result=0;
for (int i = 0; i < 1e8; ++i) {
  result+=calc2([](float arg){ return arg * 0.5f; });
}

calc2 ile

1.71799e+10, time spent 0.14 sec

calc1 ile olurken

6.6435e+10, time spent 5.772 sec

bu hız farkında ~ 40 ve değerlerde ~ 4 faktörüdür. Birincisi, OP'nin yayınladığından (görsel stüdyo kullanarak) çok daha büyük bir fark. Aslında bir son değeri yazdırmak, derleyicinin görünür bir sonuç olmadan kodu kaldırmasını önlemek için de iyi bir fikirdir (eğer kural). Cassio Neri bunu zaten cevabında söyledi. Sonuçların ne kadar farklı olduğuna dikkat edin - Farklı hesaplamalar yapan kodların hız faktörlerini karşılaştırırken dikkatli olunmalıdır.

Ayrıca, adil olmak gerekirse, f (3.3) 'ü tekrar tekrar hesaplamanın çeşitli yollarını karşılaştırmak belki de ilginç değildir. Giriş sabitse, bir döngü içinde olmamalıdır. (Optimize edicinin fark etmesi kolaydır)

Eğer calc1 ve 2'ye kullanıcı tarafından sağlanan bir değer argümanı eklersem calc1 ve calc2 arasındaki hız faktörü, 40'tan 5 faktörüne iner! Görsel stüdyo ile fark 2 faktörüne yakındır ve clang ile fark yoktur (aşağıya bakınız).

Ayrıca, çarpımlar hızlı olduğu için, yavaşlama faktörleri hakkında konuşmak çoğu zaman ilginç değildir. Daha ilginç bir soru, işlevleriniz ne kadar küçük ve bunlar gerçek bir programda darboğaz mı?

clang:

Clang (3.2 kullandım) , örnek kod için calc1 ve calc2 arasında döndüğümde aslında aynı ikili dosyaları üretti (aşağıda yayınlanmıştır). Soruda yayınlanan orijinal örnekle her ikisi de aynıdır, ancak hiç zaman almaz (döngüler yukarıda açıklandığı gibi tamamen çıkarılır). Değiştirilmiş örneğimle, -O2 ile:

Çalıştırılacak saniye sayısı (3'ün en iyisi):

clang:        calc1:           1.4 seconds
clang:        calc2:           1.4 seconds (identical binary)

gcc 4.7.2:    calc1:           1.1 seconds
gcc 4.7.2:    calc2:           6.0 seconds

VS2012 CTPNov calc1:           0.8 seconds 
VS2012 CTPNov calc2:           2.0 seconds 

VS2015 (14.0.23.107) calc1:    1.1 seconds 
VS2015 (14.0.23.107) calc2:    1.5 seconds 

MinGW (4.7.2) calc1:           0.9 seconds
MinGW (4.7.2) calc2:          20.5 seconds 

Tüm ikili dosyaların hesaplanan sonuçları aynıdır ve tüm testler aynı makinede yapılmıştır. Daha derin bir clang veya VS bilgisine sahip bir kişinin hangi optimizasyonların yapılmış olabileceği hakkında yorum yapabilmesi ilginç olurdu.

Değiştirilmiş test kodum:

#include <functional>
#include <chrono>
#include <iostream>

template <typename F>
float calc1(F f, float x) { 
  return 1.0f + 0.002*x+f(x*1.223) ; 
}

float calc2(std::function<float(float)> f,float x) { 
  return 1.0f + 0.002*x+f(x*1.223) ; 
}

int main() {
    using namespace std::chrono;

    const auto tp1 = high_resolution_clock::now();

    float result=0;
    for (int i = 0; i < 1e8; ++i) {
      result=calc1([](float arg){ 
          return arg * 0.5f; 
        },result);
    }
    const auto tp2 = high_resolution_clock::now();

    const auto d = duration_cast<milliseconds>(tp2 - tp1);  
    std::cout << d.count() << std::endl;
    std::cout << result<< std::endl;
    return 0;
}

Güncelleme:

2015'e eklendi. Ayrıca calc1, calc2'de çift float dönüşümleri olduğunu fark ettim. Bunları kaldırmak, görsel stüdyo sonucunu değiştirmez (her ikisi de çok daha hızlıdır, ancak oran yaklaşık aynıdır).


8
Bu da muhtemelen ölçütlerin yanlış olduğunu gösteriyor. İlginç kullanım durumu IMHO, çağrı kodunun başka bir yerden bir fonksiyon nesnesi aldığı yerdir, bu nedenle derleyici çağrıyı derlerken std :: fonksiyonunun kökenini bilmez. Burada, derleyici cald2 satır içi öğesini genişleterek std :: fonksiyonunun kompozisyonunu tam olarak bilir. Eylül ayında calc2 'extern' yaparak kolayca sabitlenir. Kaynak dosyası. Daha sonra elmaları portakal ile karşılaştırıyorsunuz; calc2, calc1'in yapamayacağı bir şey yapıyor. Ve döngü kireç içinde olabilir (birçok f çağrısı); fonksiyon nesnesinin ctoru etrafında değil.
greggo

1
Ne zaman uygun bir derleyiciye gidebilirim. Şimdilik diyebiliriz ki (a) gerçek bir std :: fonksiyonu için ctor 'new'i çağırır; (b) hedef eşleşen bir gerçek işlev olduğunda çağrının kendisi oldukça zayıftır; (c) bağlama olan durumlarda, adaptasyonu yapan, obj obj fonksiyonunda bir ptr kodu ile seçilen ve obj (d) 'bound' fonksiyonundan gelen verileri (bağlı parms) alan bir kod parçası vardır. derleyici görebiliyorsa, bu bağdaştırıcıya satır içine alın.
greggo

Açıklanan kurulumla yeni cevap eklendi.
greggo

3
BTW Karşılaştırma yanlış değil, soru ("std :: function vs template") yalnızca aynı derleme birimi kapsamında geçerlidir. İşlevi başka bir birime taşırsanız, şablon artık mümkün değildir, bu nedenle karşılaştırılacak bir şey yoktur.
rustyx

13

Farklı değil aynı.

Daha yavaştır, çünkü bir şablonun yapamayacağı şeyleri yapar. Özellikle, verilen argüman türleriyle çağrılabilen ve dönüş türü aynı koddan verilen dönüş türüne dönüştürülebilen herhangi bir işlevi çağırmanıza izin verir .

void eval(const std::function<int(int)>& f) {
    std::cout << f(3);
}

int f1(int i) {
    return i;
}

float f2(double d) {
    return d;
}

int main() {
    std::function<int(int)> fun(f1);
    eval(fun);
    fun = f2;
    eval(fun);
    return 0;
}

Not aynı işlevi nesne fun, her iki aramalar geçirilen eval. İki farklı işlevi vardır.

Bunu yapmak gerekmez, o zaman gerekir değil kullanmak std::function.


2
'Fun = f2' yapıldığında, 'fun' nesnesinin int'i ikiye dönüştüren, f2'yi çağıran ve çift sonucu tekrar int'e dönüştüren gizli bir işleve işaret ettiğini belirtmek isteriz (gerçek örnekte) , 'f2' bu işleve dahil edilebilir). Eğlenceye bir std :: bind atarsanız, 'fun' nesnesi bağlı parametreler için kullanılacak değerleri içerebilir. bu esnekliği desteklemek için, 'eğlence' (veya başlangıcı) ataması bellek tahsis etmeyi / yeniden yerleştirmeyi içerebilir ve gerçek çağrı yükünden çok daha uzun sürebilir.
greggo

8

Zaten burada bazı iyi cevaplar var, bu yüzden onlara çelişmeyeceğim, kısaca std :: işlevini şablonlarla karşılaştırmak sanal işlevleri işlevlerle karşılaştırmak gibidir. Sanal işlevleri hiçbir zaman işlevlere "tercih etmemelisiniz", ancak sorunlara uyduğunda sanal işlevleri kullanır ve kararları derleme süresinden çalışma süresine taşır. Fikir, ısmarlama bir çözüm (atlama tablosu gibi) kullanarak sorunu çözmek yerine derleyiciye sizin için daha iyi bir optimizasyon şansı veren bir şey kullanmanızdır. Standart bir çözüm kullanırsanız diğer programcılara da yardımcı olur.


6

Bu yanıt, std :: function çağrılarının çalışma süresi maliyeti için daha anlamlı bir kriter olduğuna inandığım mevcut cevaplar kümesine katkıda bulunmayı amaçlamaktadır.

Std :: fonksiyon mekanizması sağladığı için tanınmalıdır: Çağrılabilir herhangi bir varlık uygun imzalı bir std :: fonksiyonuna dönüştürülebilir. Bir yüzeye z = f (x, y) ile tanımlanan bir işleve uyan bir kitaplığınız olduğunu varsayalım, a'yı kabul etmek için yazabilirsiniz std::function<double(double,double)>ve kitaplık kullanıcısı herhangi bir çağrılabilir varlığı buna dönüştürebilir; sıradan bir işlev, bir sınıf örneği yöntemi veya bir lambda veya std :: bind tarafından desteklenen herhangi bir şey olsun.

Şablon yaklaşımlarının aksine, bu, farklı durumlar için kütüphane işlevini yeniden derlemeye gerek kalmadan çalışır; buna göre, her ek durum için çok az derlenmiş kod gereklidir. Bunu gerçekleştirmek her zaman mümkün olmuştur, ancak bazı garip mekanizmalar gerektiriyordu ve kütüphane kullanıcısının çalışması için işlevlerinin etrafında bir adaptör inşa etmesi gerekiyordu. std :: function , yeni ve çok güçlü bir özellik olan tüm durumlar için ortak bir çalışma zamanı çağrı arabirimi elde etmek için gereken adaptörü otomatik olarak oluşturur .

Benim görüşüme göre, bu performans açısından std :: işlevi için en önemli kullanım durumudur: Bir kez inşa edildikten sonra birçok kez bir std :: işlevini arama maliyetiyle ilgileniyorum ve derleyicinin gerçekte çağrılan işlevi bilerek çağrıyı optimize edemediği bir durum olabilir (yani uygun bir kıyaslama elde etmek için uygulamayı başka bir kaynak dosyada gizlemeniz gerekir).

Aşağıdaki testi OP'lere benzer şekilde yaptım; ancak ana değişiklikler:

  1. Her vaka 1 milyar kez döngüye girer, ancak std :: function nesneleri yalnızca bir kez oluşturulur. Gerçek std :: fonksiyon çağrıları (belki optimize edildiğinde değil) oluştururken 'operatör yeni' denilen çıkış koduna bakarak buldum.
  2. İstenmeyen optimizasyonu önlemek için test iki dosyaya bölünmüştür
  3. Benim durumlarım: (a) işlev satır içi (b) işlevi sıradan bir işlev işaretçisi tarafından geçirilir (c) işlevi std :: olarak sarılmış uyumlu bir işlevdir işlevi (d) işlevi bir std ile uyumlu yapılan uyumsuz bir işlevdir :: bağlama, std :: işlevi olarak sarılmış

Aldığım sonuçlar:

  • vaka (a) (satır içi) 1,3 ns

  • diğer tüm durumlar: 3,3 ns.

Durum (d) biraz daha yavaş olma eğilimindedir, ancak fark (yaklaşık 0,05 nsn) gürültüde emilir.

Sonuç olarak, std :: işlevi, gerçek işleve basit bir 'bağlama' uyarlaması olsa bile, bir işlev işaretçisi kullanarak karşılaştırılabilir ek yük (çağrı zamanında) olabilir. Satır içi diğerlerinden 2 ns daha hızlıdır, ancak bu beklenen bir değiş tokuştur çünkü satır içi çalışma zamanında 'kablo bağlantılı' tek durumdur.

Johan-lundberg kodunu aynı makinede çalıştırdığımda, döngü başına yaklaşık 39 nsec görüyorum, ancak std :: fonksiyonunun gerçek yapıcısı ve yıkıcısı da dahil olmak üzere döngüde çok daha fazlası var, ki bu muhtemelen oldukça yüksek çünkü yeni ve sil.

-O2 gcc 4.8.1, x86_64 hedefine (çekirdek i5).

Not, derleyicinin çağrıldığı işlevleri genişletmesini önlemek için kodun iki dosyaya bölünür (amaçlanan durum hariç).

----- ilk kaynak dosya --------------

#include <functional>


// simple funct
float func_half( float x ) { return x * 0.5; }

// func we can bind
float mul_by( float x, float scale ) { return x * scale; }

//
// func to call another func a zillion times.
//
float test_stdfunc( std::function<float(float)> const & func, int nloops ) {
    float x = 1.0;
    float y = 0.0;
    for(int i =0; i < nloops; i++ ){
        y += x;
        x = func(x);
    }
    return y;
}

// same thing with a function pointer
float test_funcptr( float (*func)(float), int nloops ) {
    float x = 1.0;
    float y = 0.0;
    for(int i =0; i < nloops; i++ ){
        y += x;
        x = func(x);
    }
    return y;
}

// same thing with inline function
float test_inline(  int nloops ) {
    float x = 1.0;
    float y = 0.0;
    for(int i =0; i < nloops; i++ ){
        y += x;
        x = func_half(x);
    }
    return y;
}

----- ikinci kaynak dosya -------------

#include <iostream>
#include <functional>
#include <chrono>

extern float func_half( float x );
extern float mul_by( float x, float scale );
extern float test_inline(  int nloops );
extern float test_stdfunc( std::function<float(float)> const & func, int nloops );
extern float test_funcptr( float (*func)(float), int nloops );

int main() {
    using namespace std::chrono;


    for(int icase = 0; icase < 4; icase ++ ){
        const auto tp1 = system_clock::now();

        float result;
        switch( icase ){
         case 0:
            result = test_inline( 1e9);
            break;
         case 1:
            result = test_funcptr( func_half, 1e9);
            break;
         case 2:
            result = test_stdfunc( func_half, 1e9);
            break;
         case 3:
            result = test_stdfunc( std::bind( mul_by, std::placeholders::_1, 0.5), 1e9);
            break;
        }
        const auto tp2 = high_resolution_clock::now();

        const auto d = duration_cast<milliseconds>(tp2 - tp1);  
        std::cout << d.count() << std::endl;
        std::cout << result<< std::endl;
    }
    return 0;
}

İlgilenenler için, derleyicinin 'mul_by' 'nin bir şamandıra (float) gibi görünmesini sağlamak için oluşturduğu adaptör - bind (mul_by, _1,0.5) olarak oluşturulan işlev çağrıldığında bu' denir ':

movq    (%rdi), %rax                ; get the std::func data
movsd   8(%rax), %xmm1              ; get the bound value (0.5)
movq    (%rax), %rdx                ; get the function to call (mul_by)
cvtpd2ps    %xmm1, %xmm1        ; convert 0.5 to 0.5f
jmp *%rdx                       ; jump to the func

(bu yüzden bağlamada 0.5f yazsaydım biraz daha hızlı olabilirdi ...) 'x' parametresinin% xmm0 olarak geldiğini ve orada kaldığını unutmayın.

Test_stdfunc çağrılmadan önce işlevin oluşturulduğu alandaki kod aşağıdadır: c ++ filt ile çalıştırın:

movl    $16, %edi
movq    $0, 32(%rsp)
call    operator new(unsigned long)      ; get 16 bytes for std::function
movsd   .LC0(%rip), %xmm1                ; get 0.5
leaq    16(%rsp), %rdi                   ; (1st parm to test_stdfunc) 
movq    mul_by(float, float), (%rax)     ; store &mul_by  in std::function
movl    $1000000000, %esi                ; (2nd parm to test_stdfunc)
movsd   %xmm1, 8(%rax)                   ; store 0.5 in std::function
movq    %rax, 16(%rsp)                   ; save ptr to allocated mem

   ;; the next two ops store pointers to generated code related to the std::function.
   ;; the first one points to the adaptor I showed above.

movq    std::_Function_handler<float (float), std::_Bind<float (*(std::_Placeholder<1>, double))(float, float)> >::_M_invoke(std::_Any_data const&, float), 40(%rsp)
movq    std::_Function_base::_Base_manager<std::_Bind<float (*(std::_Placeholder<1>, double))(float, float)> >::_M_manager(std::_Any_data&, std::_Any_data const&, std::_Manager_operation), 32(%rsp)


call    test_stdfunc(std::function<float (float)> const&, int)

1
Clang 3.4.1 x64 ile sonuçlar: (a) 1.0, (b) 0.95, (c) 2.0, (d) 5.0.
rustyx

4

Sonuçlarınızı çok ilginç buldum, bu yüzden neler olduğunu anlamak için biraz kazma yaptım. Öncelikle, diğerlerinin hesaplamanın sonuçlarına sahip olması derleyicinin programın durumunu etkilemesini söylediği gibi derleyicinin bunu optimize edeceği söylenebilir. İkincisi, geri çağırma için bir silahlandırma olarak verilen sabit bir 3.3'e sahip olmak, başka optimizasyonların olacağını sanıyorum. Bunu göz önünde bulundurarak karşılaştırma kodunuzu biraz değiştirdim.

template <typename F>
float calc1(F f, float i) { return -1.0f * f(i) + 666.0f; }
float calc2(std::function<float(float)> f, float i) { return -1.0f * f(i) + 666.0f; }
int main() {
    const auto tp1 = system_clock::now();
    for (int i = 0; i < 1e8; ++i) {
        t += calc2([&](float arg){ return arg * 0.5f + t; }, i);
    }
    const auto tp2 = high_resolution_clock::now();
}

Koddaki bu değişiklik göz önüne alındığında, gcc 4.8-O3 ile derledim ve calc1 için 330ms ve calc2 için 2702 zaman aldım. Bu yüzden şablonu kullanmak 8 kat daha hızlıydı, bu sayı bana şüpheli baktı, 8 gücünün hızı genellikle derleyicinin bir şeyi vektörleştirdiğini gösterir. Şablonlar sürümü için oluşturulan koda baktığımda açıkça vectoreized oldu

.L34:
cvtsi2ss        %edx, %xmm0
addl    $1, %edx
movaps  %xmm3, %xmm5
mulss   %xmm4, %xmm0
addss   %xmm1, %xmm0
subss   %xmm0, %xmm5
movaps  %xmm5, %xmm0
addss   %xmm1, %xmm0
cvtsi2sd        %edx, %xmm1
ucomisd %xmm1, %xmm2
ja      .L37
movss   %xmm0, 16(%rsp)

Nerede std :: işlev sürümü değildi. Bu bana mantıklı geliyor, çünkü derleyici şablonla fonksiyonun döngü boyunca asla değişmeyeceğini biliyor, ancak std :: fonksiyonunun geçmesiyle değişebilir, bunun için vektörleştirilemez.

Bu beni derleyici std :: function sürümünde aynı optimizasyonu gerçekleştirmek için alabilir olmadığını görmek için başka bir şey denemek için yol açtı. Bir fonksiyona geçmek yerine std :: fonksiyonunu global bir değişken olarak yapıyorum ve buna bir çağrı yapıyorum.

float calc3(float i) {  return -1.0f * f2(i) + 666.0f; }
std::function<float(float)> f2 = [](float arg){ return arg * 0.5f; };

int main() {
    const auto tp1 = system_clock::now();
    for (int i = 0; i < 1e8; ++i) {
        t += calc3([&](float arg){ return arg * 0.5f + t; }, i);
    }
    const auto tp2 = high_resolution_clock::now();
}

Bu sürümde derleyicinin kodu aynı şekilde vektörleştirdiğini görüyorum ve aynı kıyaslama sonuçlarını elde ediyorum.

  • şablon: 330ms
  • std :: fonksiyon: 2702ms
  • global std :: fonksiyon: 330ms

Sonuç olarak bir std :: fonksiyonunun bir şablon fonksiyonuna karşı ham hızı hemen hemen aynıdır. Bununla birlikte, optimize edicinin işini çok daha zorlaştırır.


1
Bütün mesele parametre olarak bir functor geçirmektir. Kişisel calc3durum hiç mantıklı; calc3 artık f2'yi aramak için kodlanmıştır. Tabii ki optimize edilebilir.
rustyx

Gerçekten de bunu göstermeye çalışıyordum. Bu calc3 şablona eşdeğerdir ve bu durumda etkili bir şekilde tıpkı bir şablon gibi derleme zamanı yapısıdır.
Joshua Ritterman
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.