আমি popcount
ডেটা বৃহত অ্যারে দ্রুততম উপায় খুঁজছিলাম । আমি খুব অদ্ভুত প্রভাবের মুখোমুখি হয়েছি : লুপ ভেরিয়েবল থেকে অন্যটিতে পরিবর্তন unsigned
করছিuint64_t
আমার পিসিতে 50% কর্মক্ষমতা ড্রপ করেন।
বেঞ্চমার্ক
#include <iostream>
#include <chrono>
#include <x86intrin.h>
int main(int argc, char* argv[]) {
using namespace std;
if (argc != 2) {
cerr << "usage: array_size in MB" << endl;
return -1;
}
uint64_t size = atol(argv[1])<<20;
uint64_t* buffer = new uint64_t[size/8];
char* charbuffer = reinterpret_cast<char*>(buffer);
for (unsigned i=0; i<size; ++i)
charbuffer[i] = rand()%256;
uint64_t count,duration;
chrono::time_point<chrono::system_clock> startP,endP;
{
startP = chrono::system_clock::now();
count = 0;
for( unsigned k = 0; k < 10000; k++){
// Tight unrolled loop with unsigned
for (unsigned i=0; i<size/8; i+=4) {
count += _mm_popcnt_u64(buffer[i]);
count += _mm_popcnt_u64(buffer[i+1]);
count += _mm_popcnt_u64(buffer[i+2]);
count += _mm_popcnt_u64(buffer[i+3]);
}
}
endP = chrono::system_clock::now();
duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
cout << "unsigned\t" << count << '\t' << (duration/1.0E9) << " sec \t"
<< (10000.0*size)/(duration) << " GB/s" << endl;
}
{
startP = chrono::system_clock::now();
count=0;
for( unsigned k = 0; k < 10000; k++){
// Tight unrolled loop with uint64_t
for (uint64_t i=0;i<size/8;i+=4) {
count += _mm_popcnt_u64(buffer[i]);
count += _mm_popcnt_u64(buffer[i+1]);
count += _mm_popcnt_u64(buffer[i+2]);
count += _mm_popcnt_u64(buffer[i+3]);
}
}
endP = chrono::system_clock::now();
duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
cout << "uint64_t\t" << count << '\t' << (duration/1.0E9) << " sec \t"
<< (10000.0*size)/(duration) << " GB/s" << endl;
}
free(charbuffer);
}
আপনি দেখতে পাচ্ছেন, আমরা কমান্ড লাইন থেকে পড়ার মতো আকারের x
মেগাবাইটের সাথে এলোমেলোভাবে ডেটার একটি বাফার তৈরি করি x
। এরপরে, আমরা বাফারের মাধ্যমে পুনরাবৃত্তি করি এবং popcount
পপকাউন্টটি সম্পাদন করতে x86 অন্তর্নিহীন সংস্করণ ব্যবহার করি। আরও সুনির্দিষ্ট ফলাফল পেতে আমরা 10,000 বার পপকাউন্ট করি। আমরা পপকাউন্টের জন্য সময়গুলি পরিমাপ করি। উপরের ক্ষেত্রে, অভ্যন্তরীণ লুপ পরিবর্তনশীল unsigned
, নিম্ন ক্ষেত্রে, অভ্যন্তরীণ লুপ পরিবর্তনশীল হয়uint64_t
। আমি ভেবেছিলাম এটির কোনও পার্থক্য করা উচিত নয়, তবে এর বিপরীতটি।
(একেবারে পাগল) ফলাফল
আমি এটি (g ++ সংস্করণ: উবুন্টু 4.8.2-19ubuntu1) এর মতো এটি সংকলন করছি:
g++ -O3 -march=native -std=c++11 test.cpp -o test
এখানে আমার হাসওয়েল কোর আই 7-4770 কে-এর ফলাফল কে সিপিইউ @ ৩.৫০ গিগাহার্টজ-এর ফলাফল রয়েছে, যা চলছে test 1
(সুতরাং ১ এমবি র্যান্ডম ডেটা):
- স্বাক্ষরবিহীন 41959360000 0.401554 সেকেন্ড 26.113 জিবি / সেকেন্ড
- uint64_t 41959360000 0.759822 সেকেন্ড 13.8003 জিবি / এস s
আপনি দেখুন, এর থ্রুপুট uint64_t
সংস্করণ কেবলমাত্র অর্ধেক এক unsigned
সংস্করণ! সমস্যাটি মনে হচ্ছে যে বিভিন্ন সমাবেশ উত্পন্ন হয়, তবে কেন? প্রথমত, আমি একটি সংকলক বাগ সম্পর্কে চিন্তা করেছি, তাই আমি চেষ্টা করেছি clang++
(উবুন্টু ক্ল্যাং সংস্করণ 3.4-1ubuntu3):
clang++ -O3 -march=native -std=c++11 teest.cpp -o test
ফলাফল: test 1
- স্বাক্ষরযুক্ত 41959360000 0.398293 সেকেন্ড 26.3267 জিবি / গুলি s
- uint64_t 41959360000 0.680954 সেকেন্ড 15.3986 জিবি / এস
সুতরাং, এটি প্রায় একই ফলাফল এবং এখনও অদ্ভুত। তবে এখন এটি সুপার অদ্ভুত হয়ে ওঠে। আমি ধ্রুবক দিয়ে ইনপুট থেকে পড়া বাফার আকারটি প্রতিস্থাপন করি 1
, তাই আমি পরিবর্তন করি:
uint64_t size = atol(argv[1]) << 20;
প্রতি
uint64_t size = 1 << 20;
সুতরাং, সংকলক এখন সংকলন সময় বাফার আকার জানেন। সম্ভবত এটি কিছু অপ্টিমাইজেশন যুক্ত করতে পারে! এখানে নম্বরগুলি g++
:
- স্বাক্ষরবিহীন 41959360000 0.509156 সেকেন্ড 20.5944 জিবি / গুলি
- uint64_t 41959360000 0.508673 সেকেন্ড 20.6139 জিবি / এস
এখন, উভয় সংস্করণ সমান দ্রুত are তবে unsigned
আরও ধীর হয়ে গেছে ! এটা তোলে থেকে বাদ 26
থেকে 20 GB/s
, এইভাবে একটি করার জন্য একটি ধ্রুবক রয়েছে যার মান নেতৃত্ব দ্বারা একটি অ-ধ্রুবক প্রতিস্থাপন deoptimization । সিরিয়াসলি, এখানে কী চলছে তা আমার কোনও ধারণা নেই! তবে এখন clang++
নতুন সংস্করণটি সহ:
- স্বাক্ষরবিহীন 41959360000 0.677009 সেকেন্ড 15.4884 জিবি / গুলি
- uint64_t 41959360000 0.676909 সেকেন্ড 15.4906 জিবি / এস
কিসের অপেক্ষা? এখন, উভয় সংস্করণ 15 গিগাবাইট / সেটির ধীর সংখ্যাতে নেমে গেছে । সুতরাং, একটি ধ্রুবক মান দ্বারা একটি অ ধ্রুবক প্রতিস্থাপন এমনকি ঝাঁকুনির জন্য উভয় ক্ষেত্রে ধীর কোড বাড়ে!
আইভি ব্রিজ সিপিইউ সহ একজন সহকর্মীকে আমার বেঞ্চমার্ক সংকলন করতে বলেছিলাম । তিনি একই রকম ফলাফল পেয়েছেন, সুতরাং এটি হ্যাসওয়েল বলে মনে হয় না। যেহেতু দুটি সংকলক এখানে অদ্ভুত ফলাফল দেয়, এটিও একটি সংকলক বাগ বলে মনে হয় না। আমাদের এখানে একটি এএমডি সিপিইউ নেই, তাই আমরা কেবল ইন্টেলের সাথেই পরীক্ষা করতে পারতাম।
আরও উন্মাদনা, দয়া করে!
প্রথম উদাহরণটি (যার সাথে একটি atol(argv[1])
) নিন এবং static
ভেরিয়েবলের আগে একটি রাখুন , যেমন:
static uint64_t size=atol(argv[1])<<20;
জি ++ এ আমার ফলাফল এখানে:
- স্বাক্ষরবিহীন 41959360000 0.396728 সেকেন্ড 26.4306 জিবি / সেকেন্ড
- uint64_t 41959360000 0.509484 সেকেন্ড 20.5811 জিবি / গুলি s
হ্যাঁ, আরও একটি বিকল্প । আমাদের কাছে এখনও দ্রুত 26 গিগাবাইট / সেকেন্ড রয়েছে u32
, তবে আমরা u64
কমপক্ষে 13 জিবি / এস থেকে 20 গিগাবাইট / সেটির সংস্করণে পেতে পেরেছি ! আমার কলেজের পিসিতে u64
সংস্করণটি u32
সংস্করণটির চেয়ে আরও দ্রুত হয়ে উঠেছে এবং সবার দ্রুত ফলাফল পেয়েছে । দুঃখের বিষয়, এটি কেবল কাজ করে g++
, clang++
মনে হয় না যে এটি যত্নশীল static
।
আমার প্রশ্ন
আপনি এই ফলাফল ব্যাখ্যা করতে পারেন? বিশেষ করে:
- কীভাবে
u32
এবং এর মধ্যে এইরকম পার্থক্য থাকতে পারেu64
? - একটি ধ্রুবক বাফার আকারটি কম অনুকূল কোড দিয়ে ট্রিগার দ্বারা একটি অ-ধ্রুবককে কীভাবে প্রতিস্থাপন করা যায় ?
static
কীওয়ার্ড সন্নিবেশ কীভাবেu64
লুপটিকে দ্রুততর করতে পারে? আমার কলেজের কম্পিউটারে মূল কোডের চেয়েও দ্রুত!
আমি জানি যে অপ্টিমাইজেশন একটি জটিল অঞ্চল, তবে আমি কখনও ভাবিনি যে এই ধরনের ছোট পরিবর্তনগুলি 100% পার্থক্যের দিকে নিয়ে যেতে পারে কার্যকর করার সময় এবং ধ্রুবক বাফার আকারের মতো ছোট কারণগুলি আবার ফলাফলগুলি সম্পূর্ণ মিশ্রিত করতে পারে। অবশ্যই, আমি সর্বদা এমন সংস্করণটি চাই যা 26 গিগাবাইট / সেকেন্ডে পপকাউন্ট করতে সক্ষম। আমি একমাত্র নির্ভরযোগ্য উপায়টি ভাবতে পারি তা হ'ল এই কেসটির জন্য এসেম্বলিকে অনুলিপি করুন এবং ইনলাইন এসেম্বলি ব্যবহার করুন। এই একমাত্র উপায় আমি এমন সংকলকগুলি থেকে মুক্তি পেতে পারি যা ছোট ছোট পরিবর্তনগুলিতে পাগল বলে মনে হয়। আপনি কি মনে করেন? সর্বাধিক পারফরম্যান্স সহ নির্ভরযোগ্যভাবে কোড পাওয়ার অন্য কোনও উপায় আছে কি?
বিচ্ছিন্নতা
বিভিন্ন ফলাফলের জন্য এখানে বিচ্ছিন্নতা রয়েছে:
জি ++ / ইউ 32 / নন-কনস্ট্যান্ট বুফসাইজ থেকে 26 জিবি / এস সংস্করণ :
0x400af8:
lea 0x1(%rdx),%eax
popcnt (%rbx,%rax,8),%r9
lea 0x2(%rdx),%edi
popcnt (%rbx,%rcx,8),%rax
lea 0x3(%rdx),%esi
add %r9,%rax
popcnt (%rbx,%rdi,8),%rcx
add $0x4,%edx
add %rcx,%rax
popcnt (%rbx,%rsi,8),%rcx
add %rcx,%rax
mov %edx,%ecx
add %rax,%r14
cmp %rbp,%rcx
jb 0x400af8
জি ++ / u64 / নন-কনস্ট্যান্ট বুফসাইজ থেকে 13 জিবি / এস সংস্করণ :
0x400c00:
popcnt 0x8(%rbx,%rdx,8),%rcx
popcnt (%rbx,%rdx,8),%rax
add %rcx,%rax
popcnt 0x10(%rbx,%rdx,8),%rcx
add %rcx,%rax
popcnt 0x18(%rbx,%rdx,8),%rcx
add $0x4,%rdx
add %rcx,%rax
add %rax,%r12
cmp %rbp,%rdx
jb 0x400c00
ঝনঝন ++ / u64 / নন-কনস্ট্যান্ট বুফসাইজ থেকে 15 জিবি / গুলি সংস্করণ :
0x400e50:
popcnt (%r15,%rcx,8),%rdx
add %rbx,%rdx
popcnt 0x8(%r15,%rcx,8),%rsi
add %rdx,%rsi
popcnt 0x10(%r15,%rcx,8),%rdx
add %rsi,%rdx
popcnt 0x18(%r15,%rcx,8),%rbx
add %rdx,%rbx
add $0x4,%rcx
cmp %rbp,%rcx
jb 0x400e50
জি ++ / u32 এবং u64 / কনস্ট বুফসাইজ থেকে 20 জিবি / গুলি সংস্করণ :
0x400a68:
popcnt (%rbx,%rdx,1),%rax
popcnt 0x8(%rbx,%rdx,1),%rcx
add %rax,%rcx
popcnt 0x10(%rbx,%rdx,1),%rax
add %rax,%rcx
popcnt 0x18(%rbx,%rdx,1),%rsi
add $0x20,%rdx
add %rsi,%rcx
add %rcx,%rbp
cmp $0x100000,%rdx
jne 0x400a68
ঝনঝন ++ / u32 এবং u64 / কনস্ট বুফসাইজ থেকে 15 জিবি / গুলি সংস্করণ :
0x400dd0:
popcnt (%r14,%rcx,8),%rdx
add %rbx,%rdx
popcnt 0x8(%r14,%rcx,8),%rsi
add %rdx,%rsi
popcnt 0x10(%r14,%rcx,8),%rdx
add %rsi,%rdx
popcnt 0x18(%r14,%rcx,8),%rbx
add %rdx,%rbx
add $0x4,%rcx
cmp $0x20000,%rcx
jb 0x400dd0
মজার বিষয় হল, দ্রুততম (26 জিবি / গুলি) সংস্করণটিও দীর্ঘতম! এটি একমাত্র সমাধান বলে মনে হয় যা ব্যবহার করে lea
। কিছু সংস্করণ jb
লাফানোর জন্য ব্যবহার করে, অন্যরা ব্যবহার করে jne
। তবে তা বাদে সমস্ত সংস্করণ তুলনীয় বলে মনে হচ্ছে। 100% পারফরম্যান্সের ব্যবধানটি কোথা থেকে উত্পন্ন হতে পারে তা আমি দেখতে পাচ্ছি না তবে আমি সমাবেশের সিদ্ধান্ত নেওয়ার ক্ষেত্রে খুব বেশি পারদর্শী নই। সবচেয়ে ধীরে ধীরে (13 জিবি / গুলি) সংস্করণটি খুব ছোট এবং ভাল দেখাচ্ছে। কেউ কি এই ব্যাখ্যা করতে পারেন?
পাঠ শিখেছি
এই প্রশ্নের উত্তর কী হবে তা বিবেচ্য নয়; আমি শিখেছি যে সত্যই হট লুপগুলিতে প্রতিটি বিশদ গুরুত্বপূর্ণ হতে পারে, এমনকী বিশদ যাতে হট কোডের সাথে কোনও যোগসূত্র বলে মনে হয় না । লুপ ভেরিয়েবলের জন্য কী ধরণের ব্যবহার করতে হবে তা আমি কখনই ভাবি নি, তবে আপনি যেমন দেখেন যে এইরকম একটি ছোটখাটো পরিবর্তন 100% পার্থক্য আনতে পারে ! এমনকি বাফারের স্টোরেজ ধরণের কাজটি একটি বিশাল পার্থক্য করতে পারে, যেমন আমরা সন্নিবেশ সহ দেখেছিstatic
আকারের ভেরিয়েবলের সামনে কীওয়ার্ডটি ! ভবিষ্যতে, আমি সত্যই শক্ত এবং গরম লুপগুলি লিখি যা সিস্টেমের কার্য সম্পাদনের জন্য গুরুত্বপূর্ণ writing
মজার বিষয়টি হ'ল পারফরম্যান্সের পার্থক্যটি এখনও এত বেশি যদিও আমি ইতিমধ্যে চারবার লুপটি আনরোলড করেছি। সুতরাং আপনি যদি তালিকাভুক্ত না করেও, আপনি এখনও বড় পারফরম্যান্সের বিচ্যুতির শিকার হতে পারেন। বেশ আকর্ষণীয়।