আসলে, সি ++ 11 সাল থেকে, অনুলিপি করার জন্য ব্যয়std::vector
বেশিরভাগ ক্ষেত্রেই ব্যয় হয়েছে।
তবে, আপনার মনে রাখতে হবে যে নতুন ভেক্টর তৈরির জন্য ব্যয় (তারপরে এটি ধ্বংস করে দেওয়া) এখনও বিদ্যমান এবং যখন আপনি ভেক্টরের সক্ষমতা পুনরায় ব্যবহার করতে চান তখন মান দ্বারা প্রত্যাবর্তনের পরিবর্তে আউটপুট প্যারামিটারগুলি ব্যবহার করা কার্যকর। এটি C ++ কোর গাইডলাইনগুলির F.20 এ ব্যতিক্রম হিসাবে নথিভুক্ত করা হয়েছে ।
আসুন তুলনা করা যাক:
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);
}
এখন ধরুন, আমাদের এই পদ্ধতিগুলিকে numIter
একটি শক্ত লুপে কল করতে এবং কিছু পদক্ষেপ নেওয়া দরকার need উদাহরণস্বরূপ, আসুন সমস্ত উপাদানগুলির যোগফল গণনা করা যাক।
ব্যবহার করে BuildLargeVector1
, আপনি কি করবেন:
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);
}
ব্যবহার করে BuildLargeVector2
, আপনি কি করবেন:
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);
}
প্রথম উদাহরণে, অনেক অপ্রয়োজনীয় গতিশীল বরাদ্দ / ডিএলোকেশন ঘটছে, যা দ্বিতীয় উদাহরণে পুরানো পদ্ধতিতে আউটপুট প্যারামিটার ব্যবহার করে প্রতিরোধ করা হয়েছে, ইতিমধ্যে বরাদ্দ হওয়া মেমরিটিকে পুনরায় ব্যবহার করে। এই অপ্টিমাইজেশনটি মূল্যবান কিনা তা মূল্যায়ন বা পরিবর্তনের মূল্যের ব্যয়ের তুলনায় বরাদ্দ / অবলম্বনের আপেক্ষিক ব্যয়ের উপর নির্ভর করে।
মাপকাঠি
মান সঙ্গে আসুন খেলা vecSize
এবং numIter
। আমরা ভিসি সাইজ * নামিটার অবিচ্ছিন্ন রাখব যাতে "তত্ত্ব অনুসারে" একই সময় নেওয়া উচিত (= ঠিক একই মান সহ একই সংখ্যক অ্যাসাইনমেন্ট এবং সংযোজন রয়েছে), এবং সময়ের পার্থক্য কেবলমাত্র ব্যয় থেকে আসতে পারে বরাদ্দ, ডিলোকেশন এবং ক্যাশেটির আরও ভাল ব্যবহার।
আরও সুনির্দিষ্টভাবে বলা যাক ভ্যাকসাইজ * নামিটার = 2 ^ 31 = 2147483648 ব্যবহার করুন, কারণ আমার 16 গিগাবাইট র্যাম রয়েছে এবং এই সংখ্যাটি নিশ্চিত করে যে 8 গিগাবাইটের বেশি আর বরাদ্দ করা হয়নি (আকারের (ইনট) = 4), আমি নিশ্চিত করেছিলাম যে আমি ডিস্কে অদলবদল করছি না ( অন্যান্য সমস্ত প্রোগ্রাম বন্ধ ছিল, পরীক্ষা চালানোর সময় আমার কাছে 15GB ডলার উপলব্ধ ছিল)।
কোডটি এখানে:
#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;
}
এবং ফলাফল এখানে:
$ 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
(ইন্টেল আই 7-7700 কে @ 4.20GHz; 16 জিবি ডিডিআর 4 2400 মেগাহার্টজ; কুবুন্টু 18.04)
স্বরলিপি: আমার প্ল্যাটফর্মের স্মৃতি (v) = v.size () * সাইজফ (ইনট) = v.size () * 4।
অবাক হওয়ার মতো বিষয় নয়, যখন numIter = 1
(যেমন, মেম (ভি) = 8 গিগাবাইট), সময়গুলি পুরোপুরি অভিন্ন। প্রকৃতপক্ষে, উভয় ক্ষেত্রেই আমরা একবার মাত্র 8 জিবি মেমরির বিশাল ভেক্টর বরাদ্দ করি। এটি আরও প্রমাণ করে যে বিল্ডলেজভেক্টর 1 () ব্যবহার করার সময় কোনও অনুলিপি ঘটেনি: আমার কাছে অনুলিপি করার মতো পর্যাপ্ত র্যাম নেই!
যখন numIter = 2
, দ্বিতীয় ভেক্টরকে পুনরায় বরাদ্দকরণের পরিবর্তে ভেক্টরের সক্ষমতা পুনরায় ব্যবহার করা দ্রুততর হয় 1.37x is
যখন numIter = 256
, ভেক্টর সক্ষমতা পুনরুদ্ধার করুন (বার বার 256 বার একটি ভেক্টর বরাদ্দ / বরাদ্দ দেওয়ার পরিবর্তে) 2.45x দ্রুত হয় :)
আমরা লক্ষ্য করতে পারে time1 থেকে প্রায় কাছাকাছি ধ্রুবক numIter = 1
থেকে numIter = 256
, যার মানে 8GB এক বিশাল ভেক্টর বণ্টন প্রায় কাছাকাছি 32MB এর 256 ভেক্টর বণ্টন হিসেবে ব্যয়বহুল। যাইহোক, 8 গিগাবাইটের একটি বিশাল ভেক্টর বরাদ্দ করা অবশ্যই 32MB এর একটি ভেক্টর বরাদ্দের চেয়ে বেশি ব্যয়বহুল, সুতরাং ভেক্টরের ক্ষমতা পুনরায় ব্যবহারের ফলে কর্মক্ষমতা লাভ হয়।
থেকে numIter = 512
(Mem (উ) = 16MB) এর numIter = 8M
(Mem (উ) = 1KB) মিষ্টি স্পট: উভয় পদ্ধতি হিসাবে দ্রুত ঠিক হয়, এবং দ্রুত numIter এবং vecSize অন্যান্য সব সমন্বয় থাকে। এটি সম্ভবত আমার প্রসেসরের L3 ক্যাশে আকার 8MB এর সাথে করা উচিত, যাতে ভেক্টরটি পুরোপুরি ক্যাশে পুরোপুরি ফিট করে। হঠাত্ লাফিয়ে time1
মেমো (v) = 16MB এর জন্য কেন আমি সত্যিই তা ব্যাখ্যা করছি না , যখন মেমো (ভি) = 8 এমবি হবে তার ঠিক পরে ঘটবে এটি আরও যুক্তিযুক্ত মনে হবে। লক্ষ্য করুন যে আশ্চর্যরূপে, এই মিষ্টি স্পটে, পুনরায় ব্যবহারের ক্ষমতাটি না করা আসলে কিছুটা দ্রুত! আমি সত্যিই এই ব্যাখ্যা না।
যখন numIter > 8M
জিনিসগুলি কুৎসিত হতে শুরু করে। উভয় পদ্ধতি ধীর হয়ে যায় তবে মান অনুসারে ভেক্টরটি ফেরানো আরও ধীর হয়ে যায়। সবচেয়ে খারাপ ক্ষেত্রে, কেবলমাত্র একটি int
ভ্যাক্টর সহ একটি ভেক্টর , মান দ্বারা প্রত্যাবর্তনের পরিবর্তে পুনরায় ব্যবহারের ক্ষমতাটি দ্রুত 3.3x। সম্ভবতঃ, এটি ম্যালোক () এর স্থির ব্যয়ের কারণে হয়ে থাকে যা আধিপত্য শুরু করে।
টাইম 2 এর জন্য বক্ররেখা টাইম 1 এর বক্ররের তুলনায় কীভাবে মসৃণ তা নোট করুন: কেবলমাত্র ভেক্টর ক্ষমতা পুনরায় ব্যবহার করা সাধারণত দ্রুত হয় না, তবে সম্ভবত আরও গুরুত্বপূর্ণ এটি আরও অনুমানযোগ্য ।
এছাড়াও নোট করুন যে মিষ্টি স্পটে আমরা ~ 0.5 সেকেন্ডে bit৪ বিট পূর্ণসংখ্যার ২ বিলিয়ন সংযোজন করতে পেরেছি, যা ৪.২ গিগাহার্টজ bit৪ বিট প্রসেসরের উপর বেশ অনুকূল। সমস্ত 8 টি কোর ব্যবহারের জন্য আমরা গণনার সমান্তরাল করে আরও ভাল করতে পারি (উপরের পরীক্ষাটি একবারে একটি কোর ব্যবহার করে, যা আমি সিপিইউ ব্যবহারের উপর নজরদারি করার সময় পরীক্ষাটি পুনরায় চালনার মাধ্যমে যাচাই করেছি)। মেম (ভি) = 16 কেবি, যখন এল 1 ক্যাশে (i7-7700K এর জন্য L1 ডেটা ক্যাশে 4x32kB হয়) এর ক্রমগুলির ক্রম হিসাবে সর্বাধিক সম্পাদন সম্পাদিত হয় is
অবশ্যই, পার্থক্যগুলি কম এবং কম প্রাসঙ্গিক হয়ে উঠবে যত বেশি সংখ্যক আপনাকে ডেটাতে করতে হবে utation নিচের ফলাফল যদি আমরা প্রতিস্থাপন হয় sum = std::accumulate(v.begin(), v.end(), sum);
দ্বারা for (int k : v) sum += std::sqrt(2.0*k);
:
উপসংহার
- মান দ্বারা প্রত্যাবর্তনের পরিবর্তে আউটপুট প্যারামিটারগুলি ব্যবহার করে পুনরায় ব্যবহারের ক্ষমতা দ্বারা পারফরম্যান্স লাভ সরবরাহ করতে পারে।
- একটি আধুনিক ডেস্কটপ কম্পিউটারে, এটি কেবলমাত্র বড় ভেক্টর (> 16 এমবি) এবং ছোট ভেক্টর (<1 কেবি) এর জন্য প্রযোজ্য বলে মনে হয়।
- কয়েক মিলিয়ন / বিলিয়ন ছোট ভেক্টর (<1 কেবি) বরাদ্দ এড়িয়ে চলুন। যদি সম্ভব হয় তবে পুনরায় ব্যবহার ক্ষমতা, বা আরও ভাল, আপনার আর্কিটেকচারটি আলাদাভাবে ডিজাইন করুন।
অন্যান্য প্ল্যাটফর্মগুলিতে ফলাফলগুলি পৃথক হতে পারে। যথারীতি, যদি পারফরম্যান্সের বিষয়টি গুরুত্বপূর্ণ হয় তবে আপনার নির্দিষ্ট ব্যবহারের ক্ষেত্রে মানদণ্ড লিখুন।