32৪-বিটের সাথে একটি 32-বিট লুপের কাউন্টার প্রতিস্থাপনের সাথে ইনটেল সিপিইউগুলিতে _mm_popcnt_u64 এর সাথে ক্রেজি পারফরম্যান্সের বিচ্যুতিগুলি প্রবর্তিত হয়


1423

আমি 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

মজার বিষয়টি হ'ল পারফরম্যান্সের পার্থক্যটি এখনও এত বেশি যদিও আমি ইতিমধ্যে চারবার লুপটি আনরোলড করেছি। সুতরাং আপনি যদি তালিকাভুক্ত না করেও, আপনি এখনও বড় পারফরম্যান্সের বিচ্যুতির শিকার হতে পারেন। বেশ আকর্ষণীয়।


8
অনেক মন্তব্য! আপনি এগুলি চ্যাটে দেখতে এবং আপনার নিজের পছন্দ করতে চাইলে সেখানে ছেড়ে দিতে পারেন, তবে দয়া করে এখানে আর কোনও যোগ করবেন না!
শোগ 9

3
আরো দেখুন জিসিসি ইস্যু 62011, মিথ্যা তথ্য নির্ভরতা popcnt নির্দেশনা । অন্য কেউ এটি সরবরাহ করেছেন, তবে মনে হচ্ছে এটি পরিষ্কার করার সময় হারিয়ে গেছে।
jww

আমি বলতে পারি না তবে স্ট্যাটিক সহ সংস্করণটির জন্য একটি বিপর্যয়? যদি না হয়, আপনি কি পোস্টটি সম্পাদনা করে এটি যুক্ত করতে পারেন?
কেলি এস ফরাসী

উত্তর:


1551

দোষী: মিথ্যা ডেটা নির্ভরতা (এবং সংকলক এটি সম্পর্কে অবগতও নয়)

স্যান্ডি / আইভি ব্রিজ এবং হাসওয়েল প্রসেসরের উপর, নির্দেশনা:

popcnt  src, dest

গন্তব্য নিবন্ধের উপর মিথ্যা নির্ভরতা রয়েছে বলে মনে হয় dest। যদিও নির্দেশিকা কেবল এটি লিখেছে, নির্দেশ destকার্যকর করার আগে প্রস্তুত না হওয়া পর্যন্ত অপেক্ষা করবে । এই মিথ্যা নির্ভরতা (এখন) এরেলাম এইচএসডি 146 (হাসওয়েল) এবং এসকেএল029 (স্কাইলেক) হিসাবে ইন্টেল দ্বারা নথিভুক্ত করা হয়েছে

স্কাইলেক এটি lzcntএবং এর জন্য স্থির করেtzcnt
ক্যানন লেক (এবং আইস লেক) এটির জন্য এটি স্থির করে popcnt
bsf/ এর bsrসত্যিকারের আউটপুট নির্ভরতা থাকে: ইনপুট = 0 এর জন্য আউটপুট আনমোডিং হয়। (তবে অন্তর্নিহিতগুলির সাথে সেটির সুবিধা নেওয়ার কোনও উপায় নেই - কেবল এটিএমডি এটি নথি করে এবং সংকলকরা এটি প্রকাশ করে না))

(হ্যাঁ, এই নির্দেশাবলী সমস্ত একই প্রয়োগকারী ইউনিটে চালিত হয় )।


এই নির্ভরতা কেবলমাত্র popcntএকটি লুপের পুনরাবৃত্তি থেকে 4 টি ধরে রাখে না । এটি লুপ পুনরাবৃত্তিগুলি বহন করতে পারে যাতে প্রসেসরের পক্ষে বিভিন্ন লুপ পুনরাবৃত্তিকে সমান্তরাল করা অসম্ভব করে তোলে।

unsignedবনাম uint64_tএবং অন্যান্য সমন্বয় সরাসরি সমস্যা প্রভাবিত করে না। তবে তারা রেজিস্টার বরাদ্দকারীকে প্রভাবিত করে যা ভেরিয়েবলগুলিতে নিবন্ধগুলি নিযুক্ত করে।

আপনার ক্ষেত্রে, গতি হ'ল রেজিস্টার বরাদ্দকারী কী সিদ্ধান্ত নিয়েছে তার উপর নির্ভর করে (মিথ্যা) নির্ভরতা শৃঙ্খলে কী আটকে আছে তার প্রত্যক্ষ ফলাফল।

  • 13 জিবি / এস এর একটি চেইন রয়েছে: popcnt- add- popcnt- - popcntপরবর্তী পুনরাবৃত্তি
  • 15 জিবি / এস এর একটি চেইন রয়েছে: popcnt- add- popcnt- - addপরবর্তী পুনরাবৃত্তি
  • 20 গিগাবাইট / এস এর একটি চেইন রয়েছে: popcnt- popcnt→ পরবর্তী পুনরাবৃত্তি
  • 26 গিগাবাইট / এস এর একটি চেইন রয়েছে: popcnt- popcnt→ পরবর্তী পুনরাবৃত্তি

20 গিগাবাইট / সেকেন্ড এবং 26 গিগাবাইট / এসের মধ্যে পার্থক্যটি অপ্রত্যক্ষ সম্বোধনের একটি গৌণ কারুকার্য বলে মনে হয়। যেভাবেই হোক, এই গতিতে পৌঁছানোর পরে প্রসেসরটি অন্যান্য বাধাগুলি আঘাত করতে শুরু করে।


এটি পরীক্ষা করার জন্য, আমি সংকলককে বাইপাস করতে এবং আমার পছন্দসই সমাবেশটি পাওয়ার জন্য আমি ইনলাইন সমাবেশটি ব্যবহার করেছি used আমি countঅন্যান্য সমস্ত নির্ভরতাগুলি ভেঙে দেওয়ার জন্য ভেরিয়েবলটিও বিভক্ত করেছিলাম যা বেঞ্চমার্কগুলির সাথে গোলমাল করতে পারে।

ফলাফল এখানে:

স্যান্ডি ব্রিজ জিওন @ 3.5 গিগাহার্টজ: (সম্পূর্ণ পরীক্ষার কোডটি নীচে পাওয়া যাবে)

  • জিসিসি 4.6.3: g++ popcnt.cpp -std=c++0x -O3 -save-temps -march=native
  • উবুন্টু 12

বিভিন্ন রেজিস্টার: 18.6195 গিগাবাইট / এস

.L4:
    movq    (%rbx,%rax,8), %r8
    movq    8(%rbx,%rax,8), %r9
    movq    16(%rbx,%rax,8), %r10
    movq    24(%rbx,%rax,8), %r11
    addq    $4, %rax

    popcnt %r8, %r8
    add    %r8, %rdx
    popcnt %r9, %r9
    add    %r9, %rcx
    popcnt %r10, %r10
    add    %r10, %rdi
    popcnt %r11, %r11
    add    %r11, %rsi

    cmpq    $131072, %rax
    jne .L4

একই নিবন্ধ: 8.49272 গিগাবাইট / এস

.L9:
    movq    (%rbx,%rdx,8), %r9
    movq    8(%rbx,%rdx,8), %r10
    movq    16(%rbx,%rdx,8), %r11
    movq    24(%rbx,%rdx,8), %rbp
    addq    $4, %rdx

    # This time reuse "rax" for all the popcnts.
    popcnt %r9, %rax
    add    %rax, %rcx
    popcnt %r10, %rax
    add    %rax, %rsi
    popcnt %r11, %rax
    add    %rax, %r8
    popcnt %rbp, %rax
    add    %rax, %rdi

    cmpq    $131072, %rdx
    jne .L9

ভাঙা চেইনের সাথে একই নিবন্ধন করুন: 17.8869 গিগাবাইট / এস

.L14:
    movq    (%rbx,%rdx,8), %r9
    movq    8(%rbx,%rdx,8), %r10
    movq    16(%rbx,%rdx,8), %r11
    movq    24(%rbx,%rdx,8), %rbp
    addq    $4, %rdx

    # Reuse "rax" for all the popcnts.
    xor    %rax, %rax    # Break the cross-iteration dependency by zeroing "rax".
    popcnt %r9, %rax
    add    %rax, %rcx
    popcnt %r10, %rax
    add    %rax, %rsi
    popcnt %r11, %rax
    add    %rax, %r8
    popcnt %rbp, %rax
    add    %rax, %rdi

    cmpq    $131072, %rdx
    jne .L14

তাই কি সংকলক ভুল হয়েছে?

দেখে মনে হচ্ছে যে জিসিসি বা ভিজ্যুয়াল স্টুডিও উভয়ই এটি সচেতন নয় popcnt এরকম একটি মিথ্যা নির্ভরতা রয়েছে। তবুও, এই মিথ্যা নির্ভরতা অস্বাভাবিক নয়। সংকলক এটি সম্পর্কে সচেতন কিনা তা কেবল বিষয়।

popcntহুবহু সর্বাধিক ব্যবহৃত নির্দেশনা। সুতরাং এটি কোনও আশ্চর্য নয় যে কোনও বড় সংকলক এরকম কিছু মিস করতে পারে। এই সমস্যাটির উল্লেখ করে কোথাও কোনও ডকুমেন্টেশন নেই বলে মনে হয়। যদি ইন্টেল এটি প্রকাশ না করে, তবে কেউ সুযোগ মতো এটির মধ্যে না যাওয়া পর্যন্ত বাইরের কেউ জানতে পারবে না।

( আপডেট: সংস্করণ ৪.৯.২ অনুসারে , জিসিসি এই মিথ্যা-নির্ভরতা সম্পর্কে অবগত এবং অপ্টিমাইজেশন সক্ষম করা থাকলে তা ক্ষতিপূরণ দেওয়ার কোড জেনারেট করে Cla ক্ল্যাং, এমএসভিসি এবং এমনকি ইন্টেলের নিজস্ব আইসিসি সহ অন্যান্য বিক্রেতাদের বড় সংকলক এখনও অবগত নন এই মাইক্রোআরকিটেকচারাল ইরটাম এবং এর জন্য ক্ষতিপূরণ প্রদানকারী কোড নির্গত করবে না))

সিপিইউর এমন মিথ্যা নির্ভরতা কেন?

আমরা ফটকা করতে পারেন: এটা হিসাবে একই ফাঁসি ইউনিট উপর সঞ্চালিত হয় bsf/ bsrযা না একটি আউটপুট নির্ভরতা আছে। ( হার্ডওয়্যারে POPCNT কীভাবে প্রয়োগ করা হয়? ) এই নির্দেশাবলীর জন্য, ইনটেল ইনপুট = 0 কে "অপরিজ্ঞাত" (জেডএফ = 1 সহ) হিসাবে পূর্ণসংখ্যার ফলাফলটি নথিভুক্ত করে, তবে ইন্টেল হার্ডওয়্যারটি পুরানো সফ্টওয়্যারটি ভাঙ্গা এড়ানোর জন্য প্রকৃতপক্ষে একটি শক্তিশালী গ্যারান্টি দেয়: আউটপুটটি সংশোধিত নয়। এএমডি এই আচরণের দলিল দেয়।

সম্ভবত আউটপুটের উপর নির্ভরশীল এই এক্সিকিউশন ইউনিটটির জন্য কিছু উওপ তৈরি করা কোনওভাবেই অসুবিধাজনক ছিল তবে অন্যরা তা নয়।

এএমডি প্রসেসরগুলির এই মিথ্যা নির্ভরতা রয়েছে বলে মনে হয় না।


সম্পূর্ণ পরীক্ষার কোডটি রেফারেন্সের জন্য নীচে রয়েছে:

#include <iostream>
#include <chrono>
#include <x86intrin.h>

int main(int argc, char* argv[]) {

   using namespace std;
   uint64_t size=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;
   {
      uint64_t c0 = 0;
      uint64_t c1 = 0;
      uint64_t c2 = 0;
      uint64_t c3 = 0;
      startP = chrono::system_clock::now();
      for( unsigned k = 0; k < 10000; k++){
         for (uint64_t i=0;i<size/8;i+=4) {
            uint64_t r0 = buffer[i + 0];
            uint64_t r1 = buffer[i + 1];
            uint64_t r2 = buffer[i + 2];
            uint64_t r3 = buffer[i + 3];
            __asm__(
                "popcnt %4, %4  \n\t"
                "add %4, %0     \n\t"
                "popcnt %5, %5  \n\t"
                "add %5, %1     \n\t"
                "popcnt %6, %6  \n\t"
                "add %6, %2     \n\t"
                "popcnt %7, %7  \n\t"
                "add %7, %3     \n\t"
                : "+r" (c0), "+r" (c1), "+r" (c2), "+r" (c3)
                : "r"  (r0), "r"  (r1), "r"  (r2), "r"  (r3)
            );
         }
      }
      count = c0 + c1 + c2 + c3;
      endP = chrono::system_clock::now();
      duration=chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
      cout << "No Chain\t" << count << '\t' << (duration/1.0E9) << " sec \t"
            << (10000.0*size)/(duration) << " GB/s" << endl;
   }
   {
      uint64_t c0 = 0;
      uint64_t c1 = 0;
      uint64_t c2 = 0;
      uint64_t c3 = 0;
      startP = chrono::system_clock::now();
      for( unsigned k = 0; k < 10000; k++){
         for (uint64_t i=0;i<size/8;i+=4) {
            uint64_t r0 = buffer[i + 0];
            uint64_t r1 = buffer[i + 1];
            uint64_t r2 = buffer[i + 2];
            uint64_t r3 = buffer[i + 3];
            __asm__(
                "popcnt %4, %%rax   \n\t"
                "add %%rax, %0      \n\t"
                "popcnt %5, %%rax   \n\t"
                "add %%rax, %1      \n\t"
                "popcnt %6, %%rax   \n\t"
                "add %%rax, %2      \n\t"
                "popcnt %7, %%rax   \n\t"
                "add %%rax, %3      \n\t"
                : "+r" (c0), "+r" (c1), "+r" (c2), "+r" (c3)
                : "r"  (r0), "r"  (r1), "r"  (r2), "r"  (r3)
                : "rax"
            );
         }
      }
      count = c0 + c1 + c2 + c3;
      endP = chrono::system_clock::now();
      duration=chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
      cout << "Chain 4   \t"  << count << '\t' << (duration/1.0E9) << " sec \t"
            << (10000.0*size)/(duration) << " GB/s" << endl;
   }
   {
      uint64_t c0 = 0;
      uint64_t c1 = 0;
      uint64_t c2 = 0;
      uint64_t c3 = 0;
      startP = chrono::system_clock::now();
      for( unsigned k = 0; k < 10000; k++){
         for (uint64_t i=0;i<size/8;i+=4) {
            uint64_t r0 = buffer[i + 0];
            uint64_t r1 = buffer[i + 1];
            uint64_t r2 = buffer[i + 2];
            uint64_t r3 = buffer[i + 3];
            __asm__(
                "xor %%rax, %%rax   \n\t"   // <--- Break the chain.
                "popcnt %4, %%rax   \n\t"
                "add %%rax, %0      \n\t"
                "popcnt %5, %%rax   \n\t"
                "add %%rax, %1      \n\t"
                "popcnt %6, %%rax   \n\t"
                "add %%rax, %2      \n\t"
                "popcnt %7, %%rax   \n\t"
                "add %%rax, %3      \n\t"
                : "+r" (c0), "+r" (c1), "+r" (c2), "+r" (c3)
                : "r"  (r0), "r"  (r1), "r"  (r2), "r"  (r3)
                : "rax"
            );
         }
      }
      count = c0 + c1 + c2 + c3;
      endP = chrono::system_clock::now();
      duration=chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
      cout << "Broken Chain\t"  << count << '\t' << (duration/1.0E9) << " sec \t"
            << (10000.0*size)/(duration) << " GB/s" << endl;
   }

   free(charbuffer);
}

একটি সমান আকর্ষণীয় মাপদণ্ড এখানে পাওয়া যাবে: http://pastebin.com/kbzgL8si
এই মানদণ্ডটি popcnt(মিথ্যা) নির্ভরতা শৃঙ্খলে থাকা সংখ্যার পরিবর্তিত হয় ।

False Chain 0:  41959360000 0.57748 sec     18.1578 GB/s
False Chain 1:  41959360000 0.585398 sec    17.9122 GB/s
False Chain 2:  41959360000 0.645483 sec    16.2448 GB/s
False Chain 3:  41959360000 0.929718 sec    11.2784 GB/s
False Chain 4:  41959360000 1.23572 sec     8.48557 GB/s

3
হাই বাউল! অতীতের প্রচুর মন্তব্য এখানে; নতুন ছেড়ে যাওয়ার আগে দয়া করে সংরক্ষণাগারটি পর্যালোচনা করুন
শোগ 9

1
@ জাস্টিনএল.আইট মনে হচ্ছে যে এই নির্দিষ্ট সমস্যাটি 7.0
ড্যান এম।

@ পিটারকর্ডস আমি মনে করি না এটি নির্বাহী ইউনিট যতটা শিডিয়ুলার। এটি শিডিয়ুলার যা নির্ভরতা ট্র্যাক করে। এবং এটি করার জন্য, নির্দেশাবলীকে অনেকগুলি "নির্দেশিকা ক্লাস "গুলিতে বিভক্ত করা হয় যার প্রত্যেকটি শিডিয়ুলার দ্বারা স্বতন্ত্রভাবে চিকিত্সা করা হয়। সুতরাং নির্দেশের সময়সূচির উদ্দেশ্যে 3-চক্রের সমস্ত "স্লো-ইন" নির্দেশকে একই "শ্রেণিতে" ফেলে দেওয়া হয়েছিল।
রহস্যময়

@ মিস্টিয়াল: আপনি কি এখনও তাই মনে করেন? এটি প্রশংসনীয়, তবে imul dst, src, immআউটপুট নির্ভরতা নেই এবং ধীরে ধীরেও হয় না lea। উভয়ই করেন না pdep, তবে এটি VX 2 ইনপুট অপারেডগুলির সাথে এনকোডড। সম্মত হয়েছে যে এটি নিজেই ফাঁসির একক নয় যা মিথ্যা ডেপিকে সৃষ্টি করে; এটি আরএটি অবধি এবং ইস্যু / পুনরায় নামকরণের পর্যায় পর্যন্ত এটি শারীরিক রেজিস্টারে আর্কিটেকচারাল রেজিস্ট্রার অপারামের নামকরণ করে। সম্ভবত এটির জন্য ইউওপ-কোডের একটি টেবিলের প্রয়োজন -> নির্ভরতা প্যাটার্ন এবং পোর্ট পছন্দ এবং একই এক্সিকিউশন ইউনিটের জন্য সমস্ত উফগুলিকে একসাথে করা সেই টেবিলটিকে সহজতর করে। এটাই আমি আরও বিস্তারিতভাবে বোঝাতে চাইছি।
পিটার

আপনি যদি আমাকে নিজের উত্তরে এটি সম্পাদনা করতে চান বা আপনি শিডিয়ুলার সম্পর্কে মূলত যা বলেছিলেন তেমন কিছু বলতে চাইলে তা আমাকে জানান। এসকেএল lzcnt / tzcnt এর জন্য মিথ্যা ডেপ ফেলেছে তবে পপসেন্ট নয় তা আমাদের কিছু বলা উচিত, তবে IDK কী। আর একটি সম্ভাব্য চিহ্ন যা এর নাম / আরএটি-সম্পর্কিত-নামকরণ করে তা হ'ল এসকেএল lzcnt / tzcnt এর জন্য মেমোরি উত্স হিসাবে পপকন্ট নয় তবে একটি সূচকযুক্ত ঠিকানা মোডকে মুক্ত করে। স্পষ্টতই পুনঃনামকরণ ইউনিটটি ইউওপ তৈরি করতে হবে যদিও ব্যাক-এন্ড উপস্থাপন করতে পারে।
পিটার

50

আমি পরীক্ষার জন্য একটি সমতুল্য সি কোড আপ করেছি এবং আমি এই অদ্ভুত আচরণটি নিশ্চিত করতে পারি। আরও কী, gccbelieves৪-বিট পূর্ণসংখ্যার (যা সম্ভবত size_tকোনওভাবে হওয়া উচিত ...) বিশ্বাস করা আরও ভাল, uint_fast32_tকারণ জিসিসি ব্যবহার করে 64৪-বিট ইউিন্ট ব্যবহার করা হয়।

আমি অ্যাসেম্বলিটি নিয়ে প্রায়
বিদ্রূপ ঘটিয়েছি : কেবল 32-বিট সংস্করণটি নিন, সমস্ত 32-বিট নির্দেশাবলী / রেজিস্টারগুলিকে প্রোগ্রামটির অভ্যন্তরীণ পপকাউন্ট-লুপের 64-বিট সংস্করণ দিয়ে প্রতিস্থাপন করুন। পর্যবেক্ষণ: কোডটি 32-বিট সংস্করণের মতোই দ্রুত!

এটি স্পষ্টতই একটি হ্যাক, যেহেতু ভেরিয়েবলের আকারটি সত্যই as৪ বিট না, কারণ প্রোগ্রামের অন্যান্য অংশগুলি এখনও 32-বিট সংস্করণ ব্যবহার করে, তবে যতক্ষণ না অভ্যন্তরীণ পপকাউন্ট-লুপটি কার্য সম্পাদনকে প্রভাবিত করে, এটি একটি ভাল শুরু ।

আমি তখন প্রোগ্রামটির 32-বিট সংস্করণ থেকে অভ্যন্তরীণ লুপ কোডটি অনুলিপি করে এটিকে 64 বিট পর্যন্ত হ্যাক করেছি, the৪-বিট সংস্করণের অভ্যন্তরীণ লুপের জন্য এটি প্রতিস্থাপনের জন্য রেজিস্টারগুলিতে ফিড করে রেখেছি। এই কোডটি 32-বিট সংস্করণ হিসাবে দ্রুত চলে।

আমার উপসংহারটি হ'ল এটি কম্পাইলার দ্বারা নির্ধারিত খারাপ নির্দেশিকার সময়সূচী, 32-বিট নির্দেশাবলীর প্রকৃত গতি / বিলম্বিত সুবিধা নয়।

(কেভেট: আমি অ্যাসেম্বলি হ্যাক করেছি, লক্ষ্য না করে কিছু ভেঙে ফেলতে পারতাম I আমি তা মনে করি না))


1
"আরও কি, জিসিসি believes৪-বিট পূর্ণসংখ্যার বিশ্বাস করে […] আরও ভাল হিসাবে, যেমন uint_fast32_t ব্যবহার করে জিসিসি একটি a৪-বিট ইউিন্ট ব্যবহার করে।" দুর্ভাগ্যক্রমে, এবং আমার আফসোসের জন্য, এই ধরণের পিছনে কোনও জাদু নেই এবং কোনও গভীর কোড অন্তর্মুখি নেই। আমি এখনও তাদের পুরো প্ল্যাটফর্মের প্রতিটি সম্ভাব্য স্থান এবং প্রতিটি প্রোগ্রামের জন্য একক টাইপডেফ হিসাবে অন্য কোনও উপায় সরবরাহ করতে দেখেছি। প্রকারের সঠিক পছন্দের পিছনে সম্ভবত বেশ কিছু চিন্তাভাবনা রয়েছে, তবে তাদের প্রত্যেকটির একটির সংজ্ঞা সম্ভবত সেখানে যে কোনও প্রয়োগ রয়েছে তার সাথে ফিট করে না। আরও কিছু পড়া: stackoverflow.com/q/4116297
কেনো

2
@ কেনো কারণ sizeof(uint_fast32_t)এটি সংজ্ঞায়িত করতে হবে। যদি আপনি এটি না হওয়ার অনুমতি দেন তবে আপনি সেই কৌশলটি করতে পারেন, তবে এটি কেবল একটি সংকলক এক্সটেনশনের মাধ্যমেই সম্পন্ন করা যায়।
wizzwizz4

25

এটি কোনও উত্তর নয়, তবে যদি আমি মন্তব্যটিতে ফলাফল রাখি তবে এটি পড়া শক্ত।

আমি এই ফলাফলগুলি একটি ম্যাক প্রো ( ওয়েস্টমিয়ার 6-কোরাস জিয়ন 3.33 গিগাহার্টজ) এর সাথে পেয়েছি । আমি এটি সংকলিত clang -O3 -msse4 -lstdc++ a.cpp -o a(-O2 একই ফলাফল পেতে)।

সঙ্গে ঝাঁকুনি uint64_t size=atol(argv[1])<<20;

unsigned    41950110000 0.811198 sec    12.9263 GB/s
uint64_t    41950110000 0.622884 sec    16.8342 GB/s

সঙ্গে ঝাঁকুনি uint64_t size=1<<20;

unsigned    41950110000 0.623406 sec    16.8201 GB/s
uint64_t    41950110000 0.623685 sec    16.8126 GB/s

আমি চেষ্টাও করেছি:

  1. পরীক্ষার ক্রমটি বিপরীত করুন, ফলাফলটি একই রকম তাই এটি ক্যাশে ফ্যাক্টরটিকে বিযুক্ত করে।
  2. আছে forবিপরীত বিবৃতি: for (uint64_t i=size/8;i>0;i-=4)। এটি একই ফলাফল দেয় এবং প্রমাণ করে যে সংকলনটি যথেষ্ট পরিমাণে স্মার্ট যাতে প্রতি পুনরাবৃত্তির দ্বারা 8 দ্বারা আকার বিভাজক করা যায় না (প্রত্যাশার মতো)।

এখানে আমার বন্য অনুমান:

গতি ফ্যাক্টরটি তিন ভাগে আসে:

  • কোড ক্যাশে: uint64_tসংস্করণটির বৃহত্তর কোড আকার রয়েছে, তবে এটির আমার Xeon সিপিইউতে কোনও প্রভাব নেই। এটি 64-বিট সংস্করণটিকে ধীর করে তোলে।

  • নির্দেশাবলী ব্যবহৃত। শুধুমাত্র লুপের গণনা নোট করুন, তবে দুটি সংস্করণে 32-বিট এবং 64-বিট সূচক সহ বাফার অ্যাক্সেস করা হয়েছে। একটি 64৪-বিট অফসেট সহ একটি পয়েন্টার অ্যাক্সেস একটি নিবেদিত -৪-বিট রেজিস্টার এবং ঠিকানাটির অনুরোধ করে, আপনি যখন 32-বিট অফসেটের জন্য তাত্ক্ষণিক ব্যবহার করতে পারেন। এটি 32-বিট সংস্করণটি দ্রুত তৈরি করতে পারে।

  • নির্দেশাবলী কেবলমাত্র 64-বিট সংকলন (যেটি প্রিফেচ) এ নির্গত হয়। এটি -৪-বিট দ্রুত করে তোলে।

তিনটি কারণ একত্রে পর্যবেক্ষিত আপাতদৃষ্টিতে বিরোধী ফলাফলের সাথে মেলে।


4
আকর্ষণীয়, আপনি সংকলক সংস্করণ এবং সংকলক পতাকা যোগ করতে পারেন? ভাল জিনিস যে আপনার মেশিনে, ফলাফল ঘুরে হয়, অর্থাত, u64 ব্যবহার করে দ্রুত হয় । এখনও অবধি আমার লুপ ভেরিয়েবলটি কোন ধরণের রয়েছে তা নিয়ে আমি কখনই ভাবিনি, তবে মনে হয় পরের বারের জন্য আমাকে দুবার ভাবতে হবে :)।
জেক্সিকাইড

2
@ জেজিক্সাইড: আমি এটিকে "দ্রুত" করে তুলতে 16.8201 থেকে 16.8126 এ যাওয়ার কোনও কল দেবো না।
ব্যবহারকারী541686

2
@ মেহরদাদ: আমি যে লাফটি বোঝাতে চাইছি এটি এর মধ্যে 12.9এবং 16.8তাই unsignedএখানে দ্রুত। আমার মানদণ্ডে, বিপরীতটি ছিল, অর্থাত্ 26 এর জন্য unsigned15 uint64_t
ডলার

@ জক্সাইডাইড আপনি কি বাফারকে সম্বোধন করার ক্ষেত্রে পার্থক্য লক্ষ্য করেছেন [i]?
অ-মাস্কেবল বিঘ্নিত

@ ক্যালভিন: না, আপনার মানে কী?
জেক্সিকাইড

10

আমি কোনও প্রামাণ্য উত্তর দিতে পারি না, তবে সম্ভাব্য কারণটির একটি ওভারভিউ সরবরাহ করি provide এই রেফারেন্সটি বেশ স্পষ্টভাবে দেখায় যে আপনার লুপের শরীরে নির্দেশাবলীর জন্য বিলম্ব এবং থ্রুপুট মধ্যে 3: 1 অনুপাত রয়েছে। এটি একাধিক প্রেরণের প্রভাবগুলিও দেখায়। যেহেতু আধুনিক x86 প্রসেসরে তিনটি পূর্ণসংখ্যা ইউনিট রয়েছে (দেওয়া-বা-নেওয়া), তাই সাধারণত চক্র প্রতি তিনটি নির্দেশ প্রেরণ করা সম্ভব।

সুতরাং শিখর পাইপলাইন এবং একাধিক প্রেরণ কর্মক্ষমতা এবং এই ব্যবস্থাগুলির ব্যর্থতার মধ্যে আমাদের পারফরম্যান্সে ছয়টি ফ্যাক্টর রয়েছে। এটি বেশ সুপরিচিত যে x86 টি নির্দেশের সেটটির জটিলতা বিরক্তিকর ভাঙ্গা দেখা দেওয়া বেশ সহজ করে তোলে। উপরের দস্তাবেজের একটি দুর্দান্ত উদাহরণ রয়েছে:

-৪-বিট ডান শিফটগুলির জন্য পেন্টিয়াম 4 এর পারফরম্যান্সটি খুব খারাপ। 64-বিট বাম শিফটের পাশাপাশি সমস্ত 32-বিট শিফটে গ্রহণযোগ্য পারফরম্যান্স রয়েছে। এটি প্রদর্শিত হয় যে উপরের 32 বিট থেকে ALU এর নিম্ন 32 বিট পর্যন্ত ডেটা পাথটি ভালভাবে ডিজাইন করা হয়নি।

আমি ব্যক্তিগতভাবে একটি অদ্ভুত ক্ষেত্রে ছুঁড়েছি যেখানে একটি ফর্ম-কোর চিপের একটি নির্দিষ্ট কোরের উপর গরম লুপটি যথেষ্ট ধীর গতিতে চলেছে (যদি আমি স্মরণ করি তবে এটিএমডি)। আমরা মূলটি বন্ধ করে দিয়ে মানচিত্র-হ্রাস গণনাতে আসলে আরও ভাল পারফরম্যান্স পেয়েছি।

এখানে আমার অনুমানটি পূর্ণসংখ্যা ইউনিটগুলির পক্ষে যুক্তিযুক্ত: যে popcnt, লুপের কাউন্টার এবং ঠিকানার গণনাগুলি সবেমাত্র 32-বিট প্রশস্ত কাউন্টার দিয়ে পুরো গতিতে চলতে পারে তবে 64৪-বিটের কাউন্টারটি বিতর্ক এবং পাইপলাইন স্টলের কারণ হয়ে দাঁড়ায়। যেহেতু মোট প্রায় 12 টি চক্র রয়েছে, প্রতি লুপ বডি এক্সিকিউশন প্রতি একাধিক প্রেরণ সহ 4 টি চক্র, একক স্টল যথাযথভাবে 2 এর গুণক দ্বারা রান সময়কে প্রভাবিত করতে পারে।

স্ট্যাটিক ভেরিয়েবল ব্যবহারের দ্বারা প্রবর্তিত পরিবর্তন, যা আমি অনুমান করছি যে নির্দেশাবলীর একটি সামান্য পুনর্নির্মাণের কারণ হয়ে দাঁড়ায়, 32-বিট কোডটি বিতর্ক করার জন্য কিছু টিপিং পয়েন্টে রয়েছে another

আমি জানি এই একটি কঠোর বিশ্লেষণ নয়, কিন্তু এটা হয় একটি বিশ্বাসযোগ্য ব্যাখ্যা।


2
দুর্ভাগ্যক্রমে, যেহেতু (কোর 2?) তখন থেকেই 32-বিট এবং -৪-বিট ইন্টিজার ক্রিয়াকলাপের মধ্যে গুণ / বিভাজন ছাড়া কার্যত কোনও পারফরম্যান্সের পার্থক্য নেই - যা এই কোডটিতে উপস্থিত নেই।
রহস্যময়

@ জিন: নোট করুন যে সমস্ত সংস্করণগুলি একটি রেজিস্টারে আকার সংরক্ষণ করে এবং লুপের স্ট্যাক থেকে কখনও এটি পড়বে না। সুতরাং, ঠিকানা গণনা মিশ্রণে থাকতে পারে না, অন্তত লুপের ভিতরে নয়।
জেক্সিকাইড

@ জিন: সত্যই আকর্ষণীয় ব্যাখ্যা! তবে এটি মূল ডাব্লুটিএফ পয়েন্টগুলি ব্যাখ্যা করে না: পাইপলাইন স্টলের কারণে bit৪ বিবিটি 32 বিটের চেয়ে ধীর গতির একটি বিষয়। তবে এটি যদি হয় তবে bit৪ বিট সংস্করণটি 32 বিটের চেয়ে নির্ভরযোগ্যভাবে ধীর হওয়া উচিত নয় ? পরিবর্তে, তিনটি পৃথক সংকলক সংকলন-সময়-ধ্রুবক বাফার আকারটি ব্যবহার করার সময় এমনকি 32 বিবিট সংস্করণের জন্যও ধীর কোড নির্গত করে; বাফারের আকারটি আবার স্থির করে দেওয়া জিনিসগুলিকে পুরোপুরি পরিবর্তন করে। এমনকি আমার সহকর্মীদের মেশিনেও একটি কেস ছিল (এবং ক্যালভিনের উত্তরে) যেখানে bit৪ বিট সংস্করণটি যথেষ্ট দ্রুত! এটি একেবারে অনির্দেশীয় বলে মনে হচ্ছে ..
জেক্সিকাইড 21

@ মিস্টিয়াল এটি আমার বক্তব্য। আইইউ, বাসের সময় ইত্যাদির জন্য শূন্য মতামত থাকার সময় পিক পারফরম্যান্সের কোনও পার্থক্য নেই reference রেফারেন্সটি পরিষ্কারভাবে দেখায় যে। বিতর্ক সবকিছুকে আলাদা করে তোলে। এখানে ইন্টেল কোর সাহিত্যের একটি উদাহরণ রয়েছে: "ডিজাইনে অন্তর্ভুক্ত করা একটি নতুন প্রযুক্তি হ'ল ম্যাক্রো-অপস ফিউশন, যা একটি একক মাইক্রো-অপারেশনে দুটি x86 নির্দেশকে একত্রিত করে example উদাহরণস্বরূপ, শর্তাধীন জাম্পের সাথে তুলনা করার মতো একটি সাধারণ কোড ক্রম followed দুর্ভাগ্যক্রমে, এই প্রযুক্তিটি 64-বিট মোডে কাজ করে না "" সুতরাং আমাদের কার্যকর করার গতিতে 2: 1 অনুপাত রয়েছে।
জিন 21 ই

@ জিক্সাইডাইড আমি দেখতে পাচ্ছি আপনি কী বলছেন তবে আপনি আমার চেয়ে বেশি অনুমান করছেন। আমি বলছি যে কোডটি দ্রুত চলছে, তা পাইপলাইন এবং প্রেরণ সারিতে পূর্ণ রাখছে। এই অবস্থা ভঙ্গুর। মোট তথ্য প্রবাহে 32 টি বিট যুক্ত করার মতো ছোট পরিবর্তনগুলি এবং নির্দেশ পুনর্নির্মাণ এটি ভাঙার জন্য যথেষ্ট। সংক্ষেপে, ওপি দৃser়ভাবে দাবি যে ফিডলিং এবং টেস্টিং একমাত্র এগিয়ে যাওয়ার পথ সঠিক।
জিন

10

আমি ভিজ্যুয়াল স্টুডিও 2013 এক্সপ্রেসের মাধ্যমে এটি চেষ্টা করেছিলাম , একটি সূচকের পরিবর্তে পয়েন্টার ব্যবহার করে, যা প্রক্রিয়াটি কিছুটা বাড়িয়ে তোলে। আমার সন্দেহ হয় কারণ অ্যাড্রেসিং অফসেট + রেজিস্টার + (নিবন্ধ << 3) এর পরিবর্তে অফসেট + রেজিস্টার। সি ++ কোড।

   uint64_t* bfrend = buffer+(size/8);
   uint64_t* bfrptr;

// ...

   {
      startP = chrono::system_clock::now();
      count = 0;
      for (unsigned k = 0; k < 10000; k++){
         // Tight unrolled loop with uint64_t
         for (bfrptr = buffer; bfrptr < bfrend;){
            count += __popcnt64(*bfrptr++);
            count += __popcnt64(*bfrptr++);
            count += __popcnt64(*bfrptr++);
            count += __popcnt64(*bfrptr++);
         }
      }
      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;
   }

সমাবেশ কোড: r10 = bfrptr, r15 = bfrend, আরএসআই = গণনা, rdi = বাফার, r13 = কে:

$LL5@main:
        mov     r10, rdi
        cmp     rdi, r15
        jae     SHORT $LN4@main
        npad    4
$LL2@main:
        mov     rax, QWORD PTR [r10+24]
        mov     rcx, QWORD PTR [r10+16]
        mov     r8, QWORD PTR [r10+8]
        mov     r9, QWORD PTR [r10]
        popcnt  rdx, rax
        popcnt  rax, rcx
        add     rdx, rax
        popcnt  rax, r8
        add     r10, 32
        add     rdx, rax
        popcnt  rax, r9
        add     rsi, rax
        add     rsi, rdx
        cmp     r10, r15
        jb      SHORT $LL2@main
$LN4@main:
        dec     r13
        jne     SHORT $LL5@main

9

আপনি কি জিসিসিতে যাওয়ার চেষ্টা করেছেন -funroll-loops -fprefetch-loop-arrays?

এই অতিরিক্ত অপটিমাইজেশন সহ আমি নিম্নলিখিত ফলাফলগুলি পেয়েছি:

[1829] /tmp/so_25078285 $ cat /proc/cpuinfo |grep CPU|head -n1
model name      : Intel(R) Core(TM) i3-3225 CPU @ 3.30GHz
[1829] /tmp/so_25078285 $ g++ --version|head -n1
g++ (Ubuntu/Linaro 4.7.3-1ubuntu1) 4.7.3

[1829] /tmp/so_25078285 $ g++ -O3 -march=native -std=c++11 test.cpp -o test_o3
[1829] /tmp/so_25078285 $ g++ -O3 -march=native -funroll-loops -fprefetch-loop-arrays -std=c++11     test.cpp -o test_o3_unroll_loops__and__prefetch_loop_arrays

[1829] /tmp/so_25078285 $ ./test_o3 1
unsigned        41959360000     0.595 sec       17.6231 GB/s
uint64_t        41959360000     0.898626 sec    11.6687 GB/s

[1829] /tmp/so_25078285 $ ./test_o3_unroll_loops__and__prefetch_loop_arrays 1
unsigned        41959360000     0.618222 sec    16.9612 GB/s
uint64_t        41959360000     0.407304 sec    25.7443 GB/s

3
তবে এখনও, আপনার ফলাফলগুলি সম্পূর্ণ অদ্ভুত (প্রথমে স্বাক্ষরিত দ্রুত, তারপরে uint64_t দ্রুত) কারণ এনআরোলিংয়ের ফলে মিথ্যা নির্ভরতার মূল সমস্যাটি ঠিক হয় না।
জেক্সিকাইড

7

আপনি কি হ্রাসের পদক্ষেপটি লুপের বাইরে নিয়ে যাওয়ার চেষ্টা করেছেন? এই মুহূর্তে আপনার একটি ডেটা নির্ভরতা রয়েছে যা সত্যই প্রয়োজন হয় না।

চেষ্টা করুন:

  uint64_t subset_counts[4] = {};
  for( unsigned k = 0; k < 10000; k++){
     // Tight unrolled loop with unsigned
     unsigned i=0;
     while (i < size/8) {
        subset_counts[0] += _mm_popcnt_u64(buffer[i]);
        subset_counts[1] += _mm_popcnt_u64(buffer[i+1]);
        subset_counts[2] += _mm_popcnt_u64(buffer[i+2]);
        subset_counts[3] += _mm_popcnt_u64(buffer[i+3]);
        i += 4;
     }
  }
  count = subset_counts[0] + subset_counts[1] + subset_counts[2] + subset_counts[3];

আপনার কিছু অদ্ভুত এলিয়াসিং চলছে, আমি নিশ্চিত নই যে কঠোর আলিয়াজিং বিধি মেনে চলছি।


2
আমি প্রশ্নটি পড়ার পরে এটিই প্রথম কাজ করেছি। নির্ভরতা শৃঙ্খলা ভেঙে দিন। এটি প্রমাণিত হওয়ার সাথে সাথে পারফরম্যান্সের পার্থক্য পরিবর্তন হয় না (কমপক্ষে আমার কম্পিউটারে - জিসিসি ৪.7.৩ সহ ইন্টেল হাসওয়েল)।
নিলস পিপেনব্রিংক

1
@ বেনভয়েগ: এটি কঠোরভাবে সরে যাওয়ার পক্ষে। void*এবং char*এটি দুটি ধরণের যা অ্যালাইজড হতে পারে, কারণ এগুলি অবশ্যই "মেমরির কিছু অংশে পয়েন্টার" হিসাবে বিবেচিত হয়! ডেটা নির্ভরতা অপসারণ সম্পর্কিত আপনার ধারণাটি অপ্টিমাইজেশনের জন্য দুর্দান্ত, তবে এটি প্রশ্নের উত্তর দেয় না। এবং, যেমনটি নিলস পিপেনব্রিন্ক বলেছেন, এটি কোনও পরিবর্তন করবে বলে মনে হচ্ছে না।
জেক্সিকাইড

@ জ্যাজিক্সাইড: কঠোর আলিয়াসিং নিয়মটি প্রতিসম নয়। আপনি char*অ্যাক্সেস করতে ব্যবহার করতে পারেন T[]। আপনি কোনও অ্যাক্সেস করতে নিরাপদে একটি ব্যবহার করতে পারবেন না এবং আপনার কোডটি পরে ব্যবহার করবে বলে মনে হচ্ছে। T*char[]
বেন ভয়েগট

@ বেনভয়েগ্ট: ম্যালোক ফিরে আসার পরে আপনি কখনই কোনও mallocকিছু সংরক্ষণ করতে পারবেন না void*এবং আপনি এটি ব্যাখ্যা করেছেন T[]। এবং আমি দৃ pretty়ভাবে নিশ্চিত যে void*এবং char*কঠোর আলিয়াজিং সম্পর্কে একই শব্দার্থকতা ছিল। যাইহোক, আমি অনুমান এই বেশ offtopic এখানে :)
gexicide

1
ব্যক্তিগতভাবে আমি সঠিক uint64_t* buffer = new uint64_t[size/8]; /* type is clearly uint64_t[] */ char* charbuffer=reinterpret_cast<char*>(buffer); /* aliasing a uint64_t[] with char* is safe */
উপায়টি

6

টিএল; ডিআর: __builtinপরিবর্তে আন্তঃব্যক্তি ব্যবহার করুন; তারা সাহায্য হতে পারে।

আমি gcc4.8.4 তৈরি করতে সক্ষম হয়েছি (এবং এমনকি জিসিসি.godbolt.org এ 4.7.3) __builtin_popcountllএকই সমাবেশ নির্দেশ ব্যবহার করে এটির জন্য সর্বোত্তম কোড উত্পন্ন করতে পেরেছি , তবে ভাগ্যবান হয় এবং এমন কোড তৈরিতে ঘটে যা অপ্রত্যাশিতভাবে না হয় মিথ্যা নির্ভরতা বাগের কারণে দীর্ঘ লুপ বহনকারী নির্ভরতা।

আমি আমার বেঞ্চমার্কিং কোড সম্পর্কে 100% নিশ্চিত নই, তবে objdumpআউটপুট আমার মতামত ভাগ করে নেবে বলে মনে হচ্ছে। আমি কোনও নির্দেশ ছাড়াই সংকলকটিকে আনরোল লুপ তৈরি করতে কিছু অন্যান্য কৌশল ( ++iবনাম i++) ব্যবহার করি movl(অদ্ভুত আচরণ, আমাকে অবশ্যই বলতে হবে)।

ফলাফল:

Count: 20318230000  Elapsed: 0.411156 seconds   Speed: 25.503118 GB/s

বেঞ্চমার্কিং কোড:

#include <stdint.h>
#include <stddef.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>

uint64_t builtin_popcnt(const uint64_t* buf, size_t len){
  uint64_t cnt = 0;
  for(size_t i = 0; i < len; ++i){
    cnt += __builtin_popcountll(buf[i]);
  }
  return cnt;
}

int main(int argc, char** argv){
  if(argc != 2){
    printf("Usage: %s <buffer size in MB>\n", argv[0]);
    return -1;
  }
  uint64_t size = atol(argv[1]) << 20;
  uint64_t* buffer = (uint64_t*)malloc((size/8)*sizeof(*buffer));

  // Spoil copy-on-write memory allocation on *nix
  for (size_t i = 0; i < (size / 8); i++) {
    buffer[i] = random();
  }
  uint64_t count = 0;
  clock_t tic = clock();
  for(size_t i = 0; i < 10000; ++i){
    count += builtin_popcnt(buffer, size/8);
  }
  clock_t toc = clock();
  printf("Count: %lu\tElapsed: %f seconds\tSpeed: %f GB/s\n", count, (double)(toc - tic) / CLOCKS_PER_SEC, ((10000.0*size)/(((double)(toc - tic)*1e+9) / CLOCKS_PER_SEC)));
  return 0;
}

সংকলন বিকল্পগুলি:

gcc --std=gnu99 -mpopcnt -O3 -funroll-loops -march=native bench.c -o bench

জিসিসি সংস্করণ:

gcc (Ubuntu 4.8.4-2ubuntu1~14.04.1) 4.8.4

লিনাক্স কার্নেল সংস্করণ:

3.19.0-58-generic

সিপিইউ তথ্য:

processor   : 0
vendor_id   : GenuineIntel
cpu family  : 6
model       : 70
model name  : Intel(R) Core(TM) i7-4870HQ CPU @ 2.50 GHz
stepping    : 1
microcode   : 0xf
cpu MHz     : 2494.226
cache size  : 6144 KB
physical id : 0
siblings    : 1
core id     : 0
cpu cores   : 1
apicid      : 0
initial apicid  : 0
fpu     : yes
fpu_exception   : yes
cpuid level : 13
wp      : yes
flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx rdtscp lm constant_tsc nopl xtopology nonstop_tsc eagerfpu pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm arat pln pts dtherm fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 invpcid xsaveopt
bugs        :
bogomips    : 4988.45
clflush size    : 64
cache_alignment : 64
address sizes   : 36 bits physical, 48 bits virtual
power management:

3
এটি কেবল সৌভাগ্য যে -funroll-loopsকোডটি তৈরি করতে ঘটে যা কোনও লুপ বহনকারী নির্ভরতা শৃঙ্খলে popcntমিথ্যা ডিপ দ্বারা নির্মিত বাধা হয় না bottle একটি পুরানো সংকলক সংস্করণ যা মিথ্যা নির্ভরতা সম্পর্কে জানে না ব্যবহার করা ঝুঁকিপূর্ণ। ছাড়া -funroll-loops, জিসিসি ৪.৮.৫ এর লুপটি থ্রুটপুটের পরিবর্তে পপসেন্ট ল্যাটেন্সিতে বাধা দেবে, কারণ এটির মধ্যেrdx গুন রয়েছে । জিসিসি ৪.৯.৩ দ্বারা সংকলিত একই কোডটি xor edx,edxনির্ভরতা শৃঙ্খলা ভঙ্গ করতে একটি যুক্ত করে।
পিটার কর্ডেস

3
পুরানো সংকলকগুলির সাথে আপনার কোডটি এখনও ওপি'র অভিজ্ঞতার ঠিক একই পারফরম্যান্স পরিবর্তনের পক্ষে ঝুঁকির মধ্যে রয়েছে: আপাতদৃষ্টিতে-তুচ্ছ পরিবর্তনগুলি জিসিসি কিছু ধীরে ধীরে ধীরে ধীরে ধীরে ধীরে ধীরে ধীরে ধীরে ধীরে ধীরে ধীরে ধীরে ধীরে ধীরে ধীরে ধীরে ধীরে ধীরে ধীরে ধীরে নামিয়ে আনতে পারে কারণ এর কোনও ধারণাই ছিল না কারণ এটি কোনও সমস্যা সৃষ্টি করবে। এমন কিছু বিষয় যা একজন বৃদ্ধ কম্পাইলার উপর এক ক্ষেত্রে কাজ ঘটবে খোঁজ নিচ্ছে না প্রশ্ন।
পিটার কর্ডেস

2
রেকর্ডের জন্য, x86intrin.hএর _mm_popcnt_*জিসিসি উপর ফাংশন জোরপূর্বক প্রায় চাদরে inlined হয়__builtin_popcount* ; ইনলাইনিংটি একে একে অন্যের সমতুল্য হওয়া উচিত। আমি অত্যন্ত সন্দেহ করি যে আপনি তাদের মধ্যে স্যুইচিংয়ের কারণে যে কোনও পার্থক্য দেখতে পেয়েছেন।
শ্যাডোর্যাঞ্জার

-2

সবার আগে, শিখর পারফরম্যান্সটি অনুমান করার চেষ্টা করুন - https://www.intel.com/content/dam/www/public/us/en/documents/manouts/64-ia-32-architectures-optimization-manual.pdf পরীক্ষা করুন , বিশেষত, পরিশিষ্ট সি।

আপনার ক্ষেত্রে, এটি টেবিল সি -10 যা দেখায় যে পিওপিসিএনটি নির্দেশে লেটেন্সি = 3 ক্লক এবং থ্রুপুট = 1 ঘড়ি রয়েছে। থ্রুপুট আপনার ঘড়িতে সর্বাধিক হার দেখায় (আপনার সর্বোত্তম সম্ভাব্য ব্যান্ডউইথ নম্বর পেতে পপকন্ট 64 এর ক্ষেত্রে কোর ফ্রিকোয়েন্সি এবং 8 বাইট দ্বারা গুণিত করুন)।

এখন কম্পাইলার কী করেছে তা পরীক্ষা করুন এবং লুপের সমস্ত নির্দেশাবলীর মাধ্যমে আউটপুটগুলি যোগ করুন। এটি উত্পন্ন কোডের জন্য সর্বোত্তম সম্ভাব্য অনুমান দেবে।

শেষ অবধি, লুপের নির্দেশাবলীর মধ্যে ডেটা নির্ভরতা দেখুন কারণ তারা থ্রুপুটের পরিবর্তে বিলম্ব-বৃহত্তর বিলম্বকে বাধ্য করবে - সুতরাং তথ্য প্রবাহ চেইনে একক পুনরাবৃত্তির নির্দেশাবলী বিভক্ত করুন এবং এগুলির মধ্যে বিলম্বের গণনা করুন এবং নির্বাকভাবে এগুলি থেকে সর্বোচ্চটি বেছে নিন pick এটি অ্যাকাউন্টের প্রবাহ নির্ভরতাগুলিকে বিবেচনা করে মোটামুটি অনুমান দেবে।

তবে আপনার ক্ষেত্রে, ঠিক কোডটি সঠিকভাবে লেখাই এই সমস্ত জটিলতা দূর করবে eliminate একই গণনা ভেরিয়েবলে জমা হওয়ার পরিবর্তে কেবল বিভিন্নগুলিতে জমা করুন (যেমন গণনা 0, গণনা 1, ... গণনা 8) এবং শেষে তাদের যোগফল যোগ করুন। অথবা এমনকি গণনাগুলির একটি অ্যারে তৈরি করুন [8] এবং এর উপাদানগুলিতে জমে - সম্ভবত, এটি এমনকি ভেক্টরাইজড হবে এবং আপনি আরও ভাল থ্রুপুট পাবেন।

পিএস এবং কোনও সেকেন্ডের জন্য কখনই বেঞ্চমার্ক চালাবেন না, প্রথমে কোরটি গরম করুন তারপর কমপক্ষে 10 সেকেন্ড বা আরও ভাল 100 সেকেন্ডের জন্য লুপ চালান। অন্যথায়, আপনি হার্ডওয়্যারে পাওয়ার ম্যানেজমেন্ট ফার্মওয়্যার এবং ডিভিএফএস বাস্তবায়ন পরীক্ষা করবেন :)

পিপিএস আমি শুনেছি বেঞ্চমার্কটি সত্যিকার অর্থে কতটা চালানো উচিত তা নিয়ে অবিচ্ছিন্ন বিতর্কগুলি শুনলাম। বেশিরভাগ বুদ্ধিমান লোকেরা এমনকি 10 সেকেন্ড 11 বা 12 নয় কেন তাও জিজ্ঞাসা করছেন I অনুশীলনে, আপনি কেবল যান এবং পরপর কয়েকবার বেঞ্চমার্ক চালান এবং বিচ্যুতি রেকর্ড করেন। যে IS মজার। বেশিরভাগ লোক নতুন পারফরম্যান্সের রেকর্ড ক্যাপচারের জন্য ঠিক ওএনএসইয়ের পরে উত্স পরিবর্তন এবং বেঞ্চ চালায় run সঠিক জিনিস সঠিক করুন।

এখনও বিশ্বাস হচ্ছে না? Assp1r1n3 দ্বারা মাত্র বেঞ্চমার্কের উপরের সি-সংস্করণটি ব্যবহার করুন ( https://stackoverflow.com/a/37026212/9706746 ) করুন এবং পুনরায় চেষ্টা করে 10000 এর পরিবর্তে 100 চেষ্টা করুন।

আমার 7960X শো, RETRY = 100 সহ:

গণনা: 203182300 অতিবাহিত: 0.008385 সেকেন্ড গতি: 12.505379 গিগাবাইট / সে

গণনা: 203182300 অতিবাহিত: 0.011063 সেকেন্ড গতি: 9.478225 গিগাবাইট / সে

গণনা: 203182300 অতিবাহিত: 0.011188 সেকেন্ড গতি: 9.372327 গিগাবাইট / সে

গণনা: 203182300 অতিবাহিত: 0.010393 সেকেন্ড গতি: 10.089252 গিগাবাইট / সে

গণনা: 203182300 অতিবাহিত: 0.009076 সেকেন্ড গতি: 11.553283 গিগাবাইট / সে

RETRY = 10000 সহ:

গণনা: 20318230000 অতিবাহিত: 0.661791 সেকেন্ড গতি: 15.844519 গিগাবাইট / সে

গণনা: 20318230000 অতিবাহিত: 0.665422 সেকেন্ড গতি: 15.758060 গিগাবাইট / সে

গণনা: 20318230000 অতিবাহিত: 0.660983 সেকেন্ড গতি: 15.863888 গিগাবাইট / সে

গণনা: 20318230000 অতিবাহিত: 0.665337 সেকেন্ড গতি: 15.760073 গিগাবাইট / সে

গণনা: 20318230000 অতিবাহিত: 0.662138 সেকেন্ড গতি: 15.836215 গিগাবাইট / সে

পিপিপিএস অবশেষে, "গৃহীত উত্তর" এবং অন্যান্য মিস্ত্রি ;-) এ

আসুন assp1r1n3 এর উত্তরটি ব্যবহার করি - তার 2.5Ghz কোর রয়েছে। পিওপিসিএনটির 1 টি ক্লক থ্রোহগপুট রয়েছে, তার কোডটি 64-বিট পপসেন্ট ব্যবহার করছে। সুতরাং গণিতটি তার সেটআপের জন্য 2.5Ghz * 1 ঘড়ি * 8 বাইট = 20 গিগাবাইট / এস। তিনি প্রায় 25 জিবি / সেকেন্ডে দেখতে পাচ্ছেন, সম্ভবত প্রায় 3 গিগাহার্টজ টার্বো বৃদ্ধির কারণে।

এইভাবে ark.intel.com এ যান এবং আই 7-4870HQ দেখুন: https://ark.intel.com/products/83504/Intel-Core-i7-4870HQ- প্রসেসর 6 এম- ক্যাশে- up-to--70 -GHz-? কুই = i7-4870HQ

এই কোরটি 3.7 গিগাহার্টজ পর্যন্ত চলতে পারে এবং তার হার্ডওয়ারের জন্য রিয়েল সর্বাধিক হার 29.6 গিগাবাইট / এস। তাহলে আর 4 জিবি / এস কোথায়? সম্ভবত, এটি প্রতিটি পুনরাবৃত্তির মধ্যে লুপ লজিক এবং অন্যান্য পার্শ্ববর্তী কোডগুলিতে ব্যয় করেছে।

এখন কোথায় এই মিথ্যা নির্ভরতা? হার্ডওয়্যার প্রায় শীর্ষ হারে চলে। হয়তো আমার গণিত খারাপ, এটি কখনও কখনও ঘটে :)

পিপিপিপিপিএস এখনও এইচডাব্লুয়ের ত্রুটিযুক্ত লোকেরা পরামর্শ দেয় তারা অপরাধী, সুতরাং আমি পরামর্শটি অনুসরণ করি এবং ইনলাইন asm উদাহরণ তৈরি করেছি, নীচে দেখুন।

আমার 60৯60০ এক্স-তে, প্রথম সংস্করণ (সিএনটি-তে একক আউটপুট সহ) 11MB / s এ চলেছে, দ্বিতীয় সংস্করণ (cnt0, cnt1, cnt2 এবং cnt3 এ আউটপুট সহ) 33MB / s এ চলে। এবং কেউ বলতে পারে - ভয়েলা! এটি আউটপুট নির্ভরতা।

ঠিক আছে, সম্ভবত, আমি যে বিন্দুটি তৈরি করেছি তা হ'ল এই জাতীয় কোডটি লেখার কোনও অর্থ নেই এবং এটি আউটপুট নির্ভরতার সমস্যা নয়, বোবা কোড জেনারেশন। আমরা হার্ডওয়্যার পরীক্ষা করছি না, আমরা সর্বাধিক কর্মক্ষমতা প্রকাশ করতে কোড লিখছি। আপনি আশা করতে পারেন যে এইচডাব্লু ওওর সেই "আউটপুট-নির্ভরতা" নাম বদলে রাখা এবং আড়াল করা উচিত তবে গ্যাশ, ঠিক সঠিক কাজটি করুন এবং আপনার কোনও রহস্যের মুখোমুখি হবে না।

uint64_t builtin_popcnt1a(const uint64_t* buf, size_t len) 
{
    uint64_t cnt0, cnt1, cnt2, cnt3;
    cnt0 = cnt1 = cnt2 = cnt3 = 0;
    uint64_t val = buf[0];
    #if 0
        __asm__ __volatile__ (
            "1:\n\t"
            "popcnt %2, %1\n\t"
            "popcnt %2, %1\n\t"
            "popcnt %2, %1\n\t"
            "popcnt %2, %1\n\t"
            "subq $4, %0\n\t"
            "jnz 1b\n\t"
        : "+q" (len), "=q" (cnt0)
        : "q" (val)
        :
        );
    #else
        __asm__ __volatile__ (
            "1:\n\t"
            "popcnt %5, %1\n\t"
            "popcnt %5, %2\n\t"
            "popcnt %5, %3\n\t"
            "popcnt %5, %4\n\t"
            "subq $4, %0\n\t"
            "jnz 1b\n\t"
        : "+q" (len), "=q" (cnt0), "=q" (cnt1), "=q" (cnt2), "=q" (cnt3)
        : "q" (val)
        :
        );
    #endif
    return cnt0;
}

যদি আপনি মূল ঘড়ির চক্রের (সেকেন্ডের পরিবর্তে) সময় নির্ধারণ করে থাকেন, তবে 1 সেকেন্ডের ছোট্ট সিপিইউ-ভিত্তিক লুপের জন্য যথেষ্ট সময় plenty এমনকি 100 মিমি বড় পার্থক্য সন্ধান করতে বা ইউওপ গণনাগুলির জন্য পারফ কাউন্টারগুলি পরীক্ষা করার জন্যও ঠিক। বিশেষত স্কাইলেকে, যেখানে হার্ডওয়্যার পি-স্টেট পরিচালনা তাকে লোড শুরুর পরে মাইক্রোসেকেন্ডে সর্বোচ্চ ঘড়ির গতিতে র‌্যাম্প দেয়।
পিটার কর্ডেস

ঝনঝন অ্যাভিএক্স 2 __builtin_popcountlদিয়ে স্বয়ংক্রিয়ভাবে ভেক্টরাইজ করতে vpshufbপারে এবং এটি করার জন্য সি উত্সে একাধিক সংযোজকের দরকার নেই। আমি সম্পর্কে নিশ্চিত নই _mm_popcnt_u64; এটি কেবলমাত্র AVX512-VPOPCNT- এর সাথে স্বয়ং-ভেক্টরাইজ হতে পারে। ( AVX-512 বা AVX-2 / ব্যবহার করে বড় ডেটাতে 1 বিট (জনসংখ্যার গণনা) গণনা দেখুন )
পিটার কর্ডেস

তবে যাইহোক, ইন্টেলের অপ্টিমাইজেশান ম্যানুয়ালটি দেখে সহায়তা করবে না: যেমন গৃহীত উত্তরগুলি দেখায়, সমস্যাটির জন্য অপ্রত্যাশিত আউটপুট নির্ভরতা popcnt। এটি তাদের সাম্প্রতিক কিছু মাইক্রোর্কিটেকচারের জন্য ইন্টেলের ত্রুটিযুক্ত নথিতে লিখিত আছে, তবে আমার মনে হয় সেই সময়টি ছিল না। অপ্রত্যাশিত মিথ্যা নির্ভরতা থাকলে আপনার ডিপ-চেইন বিশ্লেষণ ব্যর্থ হবে, সুতরাং এই উত্তরটি ভাল জেনেরিক পরামর্শ তবে এখানে প্রযোজ্য নয়।
পিটার কর্ডেস

1
আপনি কি আমার সাথে মজা করছেন? হাতের লিখিত এএসএম লুপে পারফরম্যান্স কাউন্টারগুলির সাথে পরীক্ষামূলকভাবে পরিমাপ করতে পারি এমন জিনিসগুলিতে আমাকে "বিশ্বাস" করতে হবে না। তারা ঠিক ঘটনা। আমি পরীক্ষা করে দেখেছি এবং স্কাইলকে lzcnt/ এর জন্য মিথ্যা নির্ভরতা স্থির করেছে tzcnt, তবে এর জন্য নয় popcnt। ইন্টেল এর এরটাম এসকেএল ০৯৯ ইন্টেল . com/ কনটেন্ট / ডেডাম / www/ public/ us/ en/ documents/… এ দেখুন । এছাড়াও, gcc.gnu.org/bugzilla/show_bug.cgi?id=62011 "সমাধান স্থির", "অবৈধ" নয়। আপনার দাবির কোনও ভিত্তি নেই যে এইচডাব্লুতে কোনও আউটপুট নির্ভরতা নেই।
পিটার কর্ডেস

1
আপনি যদি popcnt eax, edx/ এর মতো সরল লুপটি তৈরি করেন তবে আপনি dec ecx / jnzএটি প্রতি ঘড়ি প্রতি 1 এ চলবে বলে আশা করবেন, পপসেন্ট থ্রুটপুট এবং নেওয়া-শাখা থ্রুপুটটিতে বাধা। তবে এটি popcntবারবার EAX ওভাররাইট করার জন্য বিলম্বিতভাবে আটকে থাকা 3 টি ঘড়ির মধ্যে কেবল 1 টিতে চলে runs আপনার একটি স্কাইলেক রয়েছে, তাই আপনি নিজে চেষ্টা করে দেখতে পারেন।
পিটার কর্ডেস

-3

ঠিক আছে, আমি ওপি জিজ্ঞাসা করা যে সাব-প্রশ্নগুলির একটির একটি ছোট্ট উত্তর সরবরাহ করতে চাই যা বিদ্যমান প্রশ্নগুলিতে সম্বোধন করা হবে বলে মনে হয় না। ক্যাভেট, আমি কোনও পরীক্ষা বা কোড উত্পন্ন করিনি, বা বিচ্ছিন্ন করে ফেললাম না, অন্যদের পক্ষে সম্ভবত ব্যাখ্যা করার জন্য একটি ভাবনা ভাগ করে নিতে চেয়েছিলাম।

কেন staticপারফরম্যান্স পরিবর্তন?

প্রশ্নে লাইন: uint64_t size = atol(argv[1])<<20;

সংক্ষিপ্ত উত্তর

আমি অ্যাক্সেস জন্য উত্পন্ন সমাবেশ তাকান size দেখব এবং দেখব যে অ স্থির সংস্করণের জন্য পয়েন্টার দিকনির্দেশের অতিরিক্ত পদক্ষেপ রয়েছে কিনা।

দীর্ঘ উত্তর

যেহেতু ভেরিয়েবলের কেবলমাত্র একটি অনুলিপি রয়েছে তা ঘোষণা করা হয়েছিল কিনা static এবং আকার পরিবর্তন হয় না, তাই আমি থিয়োরাইজ করি যে পার্থক্যটি মেমোরির অবস্থান যেখানে ভেরিয়েবলটি কোডে ব্যবহার করা হয় সেখানে এটির পিছনে পিছনে ব্যবহৃত হয় used নিচে।

ঠিক আছে, স্পষ্টভাবে শুরু করার জন্য, মনে রাখবেন যে কোনও ফাংশনের সমস্ত স্থানীয় ভেরিয়েবল (পরামিতি সহ) স্টোরেজ হিসাবে স্ট্যাকের জন্য ব্যবহারের জন্য সরবরাহ করা হয়। এখন, স্পষ্টতই, মূল () এর স্ট্যাক ফ্রেমটি কখনই পরিষ্কার হয় না এবং কেবল একবারই উত্পন্ন হয়। ঠিক আছে, এটি তৈরি সম্পর্কে কি static? ঠিক আছে, সেক্ষেত্রে সংকলক প্রক্রিয়াটির বিশ্বব্যাপী ডেটা স্পেসে স্থান সংরক্ষণ করতে জানে তাই কোনও স্ট্যাক ফ্রেম অপসারণের মাধ্যমে স্থানটি পরিষ্কার করা যাবে না। তবুও, আমাদের কেবল একটি অবস্থান রয়েছে তাই পার্থক্য কী? আমি সন্দেহ করি যে এটি স্ট্যাকের মেমরির অবস্থানগুলি কীভাবে রেফারেন্স করা যায় তা নিয়েই করা উচিত।

সংকলকটি যখন সারণী তৈরি করে, আকারের মতো প্রাসঙ্গিক বৈশিষ্ট্যগুলির সাথে এটি কেবল একটি লেবেলের জন্য একটি এন্ট্রি করে It এটি জানে যে এটি অবশ্যই স্মৃতিতে যথাযথ স্থান সংরক্ষণ করতে পারে তবে কিছুক্ষণ পরে এটি সঠিকভাবে বেছে না নেয় জীবন্ত বিশ্লেষণ করার পরে প্রক্রিয়া এবং সম্ভবত বরাদ্দ রেজিস্টার। তাহলে লিঙ্কার কীভাবে জানতে পারে যে চূড়ান্ত সমাবেশ কোডের জন্য মেশিন কোডে কী ঠিকানা সরবরাহ করা উচিত? এটি হয় চূড়ান্ত অবস্থানটি জানে বা কীভাবে অবস্থানে পৌঁছতে জানে। স্ট্যাকের সাহায্যে, অবস্থান ভিত্তিক এক দুটি উপাদান, স্ট্যাকফ্রেমের পয়েন্টার এবং তারপরে ফ্রেমে একটি অফসেট উল্লেখ করা বেশ সহজ। এটি মূলত কারণ লিঙ্কার রানটাইমের আগে স্ট্যাকফ্রেমের অবস্থান জানতে পারে না।


2
আমার কাছে এটি অনেক বেশি সম্ভাবনা বলে মনে হয় যে staticফাংশনটির জন্য রেজিস্টার বরাদ্দকে এমনভাবে পরিবর্তন popcntকরা হয়েছে যা ইন্টেল সিপিইউগুলির ওপিতে পরীক্ষা চালিয়ে যাচ্ছিল তার মিথ্যা আউটপুট নির্ভরতা প্রভাবিত করেছিল , এমন একটি সংকলক রয়েছে যা এড়াতে জানত না। (কারণ ইন্টেল সিপিইউতে এই পারফরম্যান্সের গর্তটি এখনও আবিষ্কার করা যায়নি)) একটি সংকলক staticএকটি স্বয়ংক্রিয় স্টোরেজ ভেরিয়েবলের মতো একটি স্থানীয় ভেরিয়েবলকে একটি রেজিস্টারে রাখতে পারে , তবে যদি তারা mainকেবল একবার রান করে ধরেই অনুকূলিত না করে , তবে এটি প্রভাবিত করবে কোড-জেন (কারণ মানটি কেবল প্রথম কল দ্বারা সেট করা হয়))
পিটার কর্ডেস

1
যাইহোক, মোডগুলির মধ্যে [RIP + rel32]এবং [rsp + 42]ঠিকানার পারফরম্যান্সের পার্থক্য বেশিরভাগ ক্ষেত্রে বেশ নগণ্য for cmp dword [RIP+rel32], immediateএকটি একক লোড + সিএমপি উওপে মাইক্রো ফিউজ করতে পারে না, তবে আমি মনে করি না এটি একটি ফ্যাক্টর হতে চলেছে। আমি যেমন বলেছি, লুপগুলির অভ্যন্তরে এটি সম্ভবত কোনও রেজিস্টারে থাকতে পারে, তবে সি ++ ট্যুইচিংয়ের অর্থ বিভিন্ন সংকলক পছন্দগুলি হতে পারে।
পিটার
আমাদের সাইট ব্যবহার করে, আপনি স্বীকার করেছেন যে আপনি আমাদের কুকি নীতি এবং গোপনীয়তা নীতিটি পড়েছেন এবং বুঝতে পেরেছেন ।
Licensed under cc by-sa 3.0 with attribution required.