Gerçekten de, C ++ 11, maliyeti kopyalamastd::vector
çoğu durumda gitti.
Bununla birlikte, yeni vektörü inşa etmenin (sonra onu yok etmenin) maliyetinin hala var olduğu ve vektörün kapasitesini yeniden kullanmak istediğinizde değere göre dönmek yerine çıktı parametrelerini kullanmanın hala yararlı olduğu unutulmamalıdır. Bu, C ++ Temel Yönergelerinin F.20'sinde bir istisna olarak belgelenmiştir .
Hadi karşılaştıralım:
std::vector<int> BuildLargeVector1(size_t vecSize) {
return std::vector<int>(vecSize, 1);
}
ile:
void BuildLargeVector2(/*out*/ std::vector<int>& v, size_t vecSize) {
v.assign(vecSize, 1);
}
Şimdi, bu yöntemleri aramamız gerektiğini varsayalım numIter
sıkı bir döngü içinde kez ve bazı eylemler gerçekleştirmemiz . Örneğin, tüm elemanların toplamını hesaplayalım.
Kullanarak BuildLargeVector1
şunları yaparsınız:
size_t sum1 = 0;
for (int i = 0; i < numIter; ++i) {
std::vector<int> v = BuildLargeVector1(vecSize);
sum1 = std::accumulate(v.begin(), v.end(), sum1);
}
Kullanarak BuildLargeVector2
şunları yaparsınız:
size_t sum2 = 0;
std::vector<int> v;
for (int i = 0; i < numIter; ++i) {
BuildLargeVector2(/*out*/ v, vecSize);
sum2 = std::accumulate(v.begin(), v.end(), sum2);
}
İlk örnekte, halihazırda tahsis edilmiş belleği yeniden kullanarak eski yöntemle bir çıktı parametresi kullanılarak ikinci örnekte önlenen birçok gereksiz dinamik ayırma / serbest bırakma gerçekleşmektedir. Bu optimizasyonun yapmaya değip değmeyeceği, değerlerin hesaplanması / değiştirilmesinin maliyeti ile karşılaştırıldığında tahsis / serbest bırakmanın göreceli maliyetine bağlıdır.
Kıyaslama
Değerleri ile Let oyun vecSize
ve numIter
. VecSize * numIter sabit tutacağız, böylece "teoride", aynı zamanı almalı (= aynı değerlere sahip aynı sayıda atama ve ekleme var) ve zaman farkı yalnızca maliyetten gelebilir ayırmalar, serbest bırakmalar ve önbelleğin daha iyi kullanımı.
Daha spesifik olarak, vecSize * numIter = 2 ^ 31 = 2147483648'i kullanalım, çünkü 16GB RAM'im var ve bu sayı, 8GB'den daha fazla ayrılmamasını sağlıyor (sizeof (int) = 4), diske takas etmememi sağlıyor ( diğer tüm programlar kapatıldı, testi çalıştırırken ~ 15GB boş alanım vardı).
İşte kod:
#include <chrono>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <vector>
class Timer {
using clock = std::chrono::steady_clock;
using seconds = std::chrono::duration<double>;
clock::time_point t_;
public:
void tic() { t_ = clock::now(); }
double toc() const { return seconds(clock::now() - t_).count(); }
};
std::vector<int> BuildLargeVector1(size_t vecSize) {
return std::vector<int>(vecSize, 1);
}
void BuildLargeVector2(/*out*/ std::vector<int>& v, size_t vecSize) {
v.assign(vecSize, 1);
}
int main() {
Timer t;
size_t vecSize = size_t(1) << 31;
size_t numIter = 1;
std::cout << std::setw(10) << "vecSize" << ", "
<< std::setw(10) << "numIter" << ", "
<< std::setw(10) << "time1" << ", "
<< std::setw(10) << "time2" << ", "
<< std::setw(10) << "sum1" << ", "
<< std::setw(10) << "sum2" << "\n";
while (vecSize > 0) {
t.tic();
size_t sum1 = 0;
{
for (int i = 0; i < numIter; ++i) {
std::vector<int> v = BuildLargeVector1(vecSize);
sum1 = std::accumulate(v.begin(), v.end(), sum1);
}
}
double time1 = t.toc();
t.tic();
size_t sum2 = 0;
{
std::vector<int> v;
for (int i = 0; i < numIter; ++i) {
BuildLargeVector2(/*out*/ v, vecSize);
sum2 = std::accumulate(v.begin(), v.end(), sum2);
}
} // deallocate v
double time2 = t.toc();
std::cout << std::setw(10) << vecSize << ", "
<< std::setw(10) << numIter << ", "
<< std::setw(10) << std::fixed << time1 << ", "
<< std::setw(10) << std::fixed << time2 << ", "
<< std::setw(10) << sum1 << ", "
<< std::setw(10) << sum2 << "\n";
vecSize /= 2;
numIter *= 2;
}
return 0;
}
Ve işte sonuç:
$ g++ -std=c++11 -O3 main.cpp && ./a.out
vecSize, numIter, time1, time2, sum1, sum2
2147483648, 1, 2.360384, 2.356355, 2147483648, 2147483648
1073741824, 2, 2.365807, 1.732609, 2147483648, 2147483648
536870912, 4, 2.373231, 1.420104, 2147483648, 2147483648
268435456, 8, 2.383480, 1.261789, 2147483648, 2147483648
134217728, 16, 2.395904, 1.179340, 2147483648, 2147483648
67108864, 32, 2.408513, 1.131662, 2147483648, 2147483648
33554432, 64, 2.416114, 1.097719, 2147483648, 2147483648
16777216, 128, 2.431061, 1.060238, 2147483648, 2147483648
8388608, 256, 2.448200, 0.998743, 2147483648, 2147483648
4194304, 512, 0.884540, 0.875196, 2147483648, 2147483648
2097152, 1024, 0.712911, 0.716124, 2147483648, 2147483648
1048576, 2048, 0.552157, 0.603028, 2147483648, 2147483648
524288, 4096, 0.549749, 0.602881, 2147483648, 2147483648
262144, 8192, 0.547767, 0.604248, 2147483648, 2147483648
131072, 16384, 0.537548, 0.603802, 2147483648, 2147483648
65536, 32768, 0.524037, 0.600768, 2147483648, 2147483648
32768, 65536, 0.526727, 0.598521, 2147483648, 2147483648
16384, 131072, 0.515227, 0.599254, 2147483648, 2147483648
8192, 262144, 0.540541, 0.600642, 2147483648, 2147483648
4096, 524288, 0.495638, 0.603396, 2147483648, 2147483648
2048, 1048576, 0.512905, 0.609594, 2147483648, 2147483648
1024, 2097152, 0.548257, 0.622393, 2147483648, 2147483648
512, 4194304, 0.616906, 0.647442, 2147483648, 2147483648
256, 8388608, 0.571628, 0.629563, 2147483648, 2147483648
128, 16777216, 0.846666, 0.657051, 2147483648, 2147483648
64, 33554432, 0.853286, 0.724897, 2147483648, 2147483648
32, 67108864, 1.232520, 0.851337, 2147483648, 2147483648
16, 134217728, 1.982755, 1.079628, 2147483648, 2147483648
8, 268435456, 3.483588, 1.673199, 2147483648, 2147483648
4, 536870912, 5.724022, 2.150334, 2147483648, 2147483648
2, 1073741824, 10.285453, 3.583777, 2147483648, 2147483648
1, 2147483648, 20.552860, 6.214054, 2147483648, 2147483648
(Intel i7-7700K @ 4.20GHz; 16GB DDR4 2400Mhz; Kubuntu 18.04)
Gösterim: mem (v) = v.size () * sizeof (int) = v.size () * 4 platformumda.
Şaşırtıcı olmayan bir şekilde, numIter = 1
(yani mem (v) = 8GB) olduğunda, zamanlar tamamen aynıdır. Aslında, her iki durumda da belleğe sadece bir kez 8GB'lık devasa bir vektör ayırıyoruz. Bu aynı zamanda BuildLargeVector1 () kullanılırken hiçbir kopya olmadığını kanıtlıyor: Kopyayı yapmak için yeterli RAM'e sahip olmayacaktım!
Ne zaman numIter = 2
yerine vektör kapasitesini yeniden ikinci bir vektör 1.37x daha hızlı yeniden tahsis.
Ne zaman numIter = 256
, (... tekrar tekrar 256 kez vektör ayırmayı kaldırma / yerine tahsis) vektörü kapasitesini yeniden hızlı 2.45x olduğunu :)
Biz zm1 den oldukça fazla sabit olduğunu fark olabilir numIter = 1
için numIter = 256
8 GB bir büyük vektör tahsis oldukça fazla 32MB 256 vektörleri tahsis olarak pahalı olduğu anlamına gelir. Bununla birlikte, 8GB'lık devasa bir vektör tahsis etmek, 32MB'lık bir vektör tahsis etmekten kesinlikle daha pahalıdır, bu nedenle vektörün kapasitesinin yeniden kullanılması performans kazanımları sağlar.
Gönderen numIter = 512
(Mem (v) = 16MB) için numIter = 8M
(Mem (v) = 1kb) tatlı nokta: iki yöntem de daha hızlı numIter ve vecSize tüm diğer kombinasyonlardan daha hızlı tam, ve. Bunun muhtemelen işlemcimin L3 önbellek boyutunun 8MB olmasıyla ilgisi var, böylece vektör hemen hemen tamamen önbelleğe sığacak. Ani sıçramanın neden time1
mem (v) = 16MB için olduğunu gerçekten açıklamıyorum , mem (v) = 8MB olduğunda hemen sonra gerçekleşmesi daha mantıklı görünüyor. Şaşırtıcı bir şekilde, bu tatlı noktada, kapasiteyi yeniden kullanmamanın aslında biraz daha hızlı olduğunu unutmayın! Bunu gerçekten açıklamıyorum.
Ne zaman numIter > 8M
işler çirkin olsun başlar. Her iki yöntem de yavaşlar ancak vektörü değere göre döndürmek daha da yavaşlar. En kötü durumda, tek bir tek içeren bir vektörle int
, değere göre dönmek yerine kapasiteyi yeniden kullanmak 3,3 kat daha hızlıdır. Muhtemelen bu, hakim olmaya başlayan malloc () 'un sabit maliyetlerinden kaynaklanmaktadır.
Time2 eğrisinin time1 eğrisinden nasıl daha pürüzsüz olduğuna dikkat edin: sadece vektör kapasitesinin yeniden kullanılması genel olarak daha hızlı değil, belki daha da önemlisi, daha öngörülebilirdir .
Ayrıca tatlı noktada ~ 0,5 saniyede 2 milyar 64 bit tam sayı eklemeyi gerçekleştirebildik, bu da 4.2Ghz 64bit işlemcide oldukça ideal. Tüm 8 çekirdeği kullanmak için hesaplamayı paralel hale getirerek daha iyisini yapabilirdik (yukarıdaki test bir seferde yalnızca bir çekirdek kullanıyor, bunu CPU kullanımını izlerken testi yeniden çalıştırarak doğruladım). En iyi performans, L1 önbelleğinin büyüklük sırası olan mem (v) = 16kB olduğunda elde edilir (i7-7700K için L1 veri önbelleği 4x32kB'dir).
Elbette, veriler üzerinde gerçekte ne kadar çok hesaplama yapmanız gerekiyorsa, farklılıklar giderek daha az alakalı hale gelir. Biz değiştirirseniz Aşağıda sonuçlarıdır sum = std::accumulate(v.begin(), v.end(), sum);
tarafından for (int k : v) sum += std::sqrt(2.0*k);
:
Sonuçlar
- Çıkış parametreleri kullanarak yerine değeriyle dönen edebilir yeniden kullanarak kapasite ile performans kazançlarını sağlarlar.
- Modern bir masaüstü bilgisayarda, bu yalnızca büyük vektörler (> 16MB) ve küçük vektörler (<1kB) için geçerli görünüyor.
- Milyonlarca / milyarlarca küçük vektörü (<1kB) ayırmaktan kaçının. Mümkünse kapasiteyi yeniden kullanın veya daha iyisi mimarinizi farklı şekilde tasarlayın.
Sonuçlar diğer platformlarda farklılık gösterebilir. Her zaman olduğu gibi, performans önemliyse, özel kullanım durumunuz için ölçütler yazın.