c - কেন এই কোড 6.5x অপ্টিমাইজেশান সঙ্গে ধীর গতিশীল?




performance gcc (2)

Godbolt এর কম্পাইলার এক্সপ্লোরারে আপনার কোড পরীক্ষা করা এই ব্যাখ্যা প্রদান করে:

  • at -O0 বা -O0 ছাড়া, জেনারেট কোডটি C লাইব্রেরী ফাংশন স্ট্রলকে কল করে
  • at -O1 জেনারেট কোড একটি rep scasb নির্দেশনা ব্যবহার করে একটি সহজ ইনলাইন সম্প্রসারণ ব্যবহার করে।
  • -O2 এবং উপরে, জেনারেট কোড আরও বিস্তৃত ইনলাইন সম্প্রসারণ ব্যবহার করে।

আপনার কোড বেনচার্কিং বারবার এক রান থেকে অন্য একটি উল্লেখযোগ্য পরিবর্তন দেখায়, কিন্তু পুনরাবৃত্তি সংখ্যা বৃদ্ধি দেখায় যে:

  • -O1 কোডটি সি লাইব্রেরির বাস্তবায়ন চেয়ে অনেক 32240 : 32240 বনাম 3090
  • -O2 কোডটি -O2 চেয়ে দ্রুত তবে এখনও C লাইব্রেরির কোডের তুলনায় উল্লেখযোগ্য ধীর: 8570 বনাম 3090

এই আচরণ gcc এবং glibc নির্দিষ্ট। clang এবং অ্যাপল এর লিঙ্কে একই রকম পরীক্ষাটি একটি উল্লেখযোগ্য পার্থক্য দেখায় না, যা কোনও আশ্চর্যের বিষয় নয় যে গডবোল্ট দেখায় যে clang সমস্ত অপ্টিমাইজেশান স্তরগুলিতে সি লাইব্রেরী স্ট্রেনের একটি কল তৈরি করে।

এটি gcc / glibc- এ একটি বাগ হিসাবে বিবেচিত হতে পারে তবে আরও বিস্তৃত বেঞ্চমার্কিং দেখাতে পারে যে strlen কলিংয়ের ওভারহেডটি ছোট স্ট্রিংয়ের জন্য ইনলাইন কোডের কর্মক্ষমতা অভাবের চেয়ে বেশি গুরুত্বপূর্ণ প্রভাব ফেলে। আপনি বেঞ্চমার্ক যা স্ট্রিং অস্বাভাবিক বড়, তাই অতি দীর্ঘ স্ট্রিং উপর বেঞ্চমার্ক ফোকাস অর্থপূর্ণ ফলাফল দিতে পারে না।

আমি এই বেঞ্চমার্ক উন্নত এবং বিভিন্ন স্ট্রিং দৈর্ঘ্য পরীক্ষা। এটি একটি ইন্টেল (আর) কোর (টিএম) i3-2100 CPU @ 3.10GHz এ চলমান gcc (ডেবিয়ান 4.7.2-5) 4.7.2 এর সাথে লিনাক্সের বেঞ্চমার্কগুলি থেকে প্রদর্শিত হয় যে -O1 দ্বারা উত্পন্ন ইনলাইন কোড সর্বদা ধীর, মাঝারি লম্বা লম্বা স্ট্রিংগুলির জন্য 10 এর একটি ফ্যাক্টর হিসাবে, যখন -O2 লম্বা স্ট্রিংগুলির জন্য খুব ছোট স্ট্রিং এবং দ্রুত অর্ধেকের জন্য -O2 চেয়ে সামান্য দ্রুত। এই তথ্য থেকে, স্ট্রেনের GNU C লাইব্রেরি সংস্করণ অন্তত আমার নির্দিষ্ট হার্ডওয়্যারগুলিতে সর্বাধিক স্ট্রিং লেন্থগুলির জন্য বেশ কার্যকর। এছাড়াও cacheing বেঞ্চমার্ক পরিমাপ উপর একটি বড় প্রভাব আছে মনে রাখা।

এখানে আপডেট কোড:

#include <stdlib.h>
#include <string.h>
#include <time.h>

void benchmark(int repeat, int minlen, int maxlen) {
    char *s = malloc(maxlen + 1);
    memset(s, 'A', minlen);
    long long bytes = 0, calls = 0;
    clock_t clk = clock();
    for (int n = 0; n < repeat; n++) {
        for (int i = minlen; i < maxlen; ++i) {
            bytes += i + 1;
            calls += 1;
            s[i] = '\0';
            s[strlen(s)] = 'A';
        }
    }
    clk = clock() - clk;
    free(s);
    double avglen = (minlen + maxlen - 1) / 2.0;
    double ns = (double)clk * 1e9 / CLOCKS_PER_SEC;
    printf("average length %7.0f -> avg time: %7.3f ns/byte, %7.3f ns/call\n",
           avglen, ns / bytes, ns / calls);
}

int main() {
    benchmark(10000000, 0, 1);
    benchmark(1000000, 0, 10);
    benchmark(1000000, 5, 15);
    benchmark(100000, 0, 100);
    benchmark(100000, 50, 150);
    benchmark(10000, 0, 1000);
    benchmark(10000, 500, 1500);
    benchmark(1000, 0, 10000);
    benchmark(1000, 5000, 15000);
    benchmark(100, 1000000 - 50, 1000000 + 50);
    return 0;
}

এখানে আউটপুট হয়:

chqrlie> gcc -std=c99 -O0 benchstrlen.c && ./a.out
average length       0 -> avg time:  14.000 ns/byte,  14.000 ns/call
average length       4 -> avg time:   2.364 ns/byte,  13.000 ns/call
average length      10 -> avg time:   1.238 ns/byte,  13.000 ns/call
average length      50 -> avg time:   0.317 ns/byte,  16.000 ns/call
average length     100 -> avg time:   0.169 ns/byte,  17.000 ns/call
average length     500 -> avg time:   0.074 ns/byte,  37.000 ns/call
average length    1000 -> avg time:   0.068 ns/byte,  68.000 ns/call
average length    5000 -> avg time:   0.064 ns/byte, 318.000 ns/call
average length   10000 -> avg time:   0.062 ns/byte, 622.000 ns/call
average length 1000000 -> avg time:   0.062 ns/byte, 62000.000 ns/call
chqrlie> gcc -std=c99 -O1 benchstrlen.c && ./a.out
average length       0 -> avg time:  20.000 ns/byte,  20.000 ns/call
average length       4 -> avg time:   3.818 ns/byte,  21.000 ns/call
average length      10 -> avg time:   2.190 ns/byte,  23.000 ns/call
average length      50 -> avg time:   0.990 ns/byte,  50.000 ns/call
average length     100 -> avg time:   0.816 ns/byte,  82.000 ns/call
average length     500 -> avg time:   0.679 ns/byte, 340.000 ns/call
average length    1000 -> avg time:   0.664 ns/byte, 664.000 ns/call
average length    5000 -> avg time:   0.651 ns/byte, 3254.000 ns/call
average length   10000 -> avg time:   0.649 ns/byte, 6491.000 ns/call
average length 1000000 -> avg time:   0.648 ns/byte, 648000.000 ns/call
chqrlie> gcc -std=c99 -O2 benchstrlen.c && ./a.out
average length       0 -> avg time:  10.000 ns/byte,  10.000 ns/call
average length       4 -> avg time:   2.000 ns/byte,  11.000 ns/call
average length      10 -> avg time:   1.048 ns/byte,  11.000 ns/call
average length      50 -> avg time:   0.337 ns/byte,  17.000 ns/call
average length     100 -> avg time:   0.299 ns/byte,  30.000 ns/call
average length     500 -> avg time:   0.202 ns/byte, 101.000 ns/call
average length    1000 -> avg time:   0.188 ns/byte, 188.000 ns/call
average length    5000 -> avg time:   0.174 ns/byte, 868.000 ns/call
average length   10000 -> avg time:   0.172 ns/byte, 1716.000 ns/call
average length 1000000 -> avg time:   0.172 ns/byte, 172000.000 ns/call

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

এখানে আমার কোড:

#include <time.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

int main() {
    char *s = calloc(1 << 20, 1);
    memset(s, 65, 1000000);
    clock_t start = clock();
    for (int i = 0; i < 128; ++i) {
        s[strlen(s)] = 'A';
    }
    clock_t end = clock();
    printf("%lld\n", (long long)(end-start));
    return 0;
}

আমার মেশিনে এটি আউটপুট:

$ gcc test.c && ./a.out
13336
$ gcc -O1 test.c && ./a.out
199004
$ gcc -O2 test.c && ./a.out
83415
$ gcc -O3 test.c && ./a.out
83415

যেহেতু, অপটিমাইজেশন সক্ষম করার ফলে এটি আরও কার্যকর হতে পারে।


GCC এর ইনলাইন pcmpeqb pmovmskb pcmpeqb / pmovmskb , এবং pmovmskb , যা pmovmskb থেকে 16-বাইট সারিবদ্ধকরণের সাথে করতে পারে তার চেয়ে অনেক ধীর । এই "অপ্টিমাইজেশান" আসলে একটি হতাশাজনক।

আমার সহজ হাতের লেখা লুপটি 16-বাইটের সারিবদ্ধতার সুবিধা নেয় যা বৃহত বাফারগুলির জন্য gcc- -O3 ইনলাইনগুলির চেয়ে 5x দ্রুত এবং ছোট স্ট্রিংগুলির জন্য ~ 2x দ্রুত। (এবং ছোট স্ট্রিং জন্য strlen কলিং চেয়ে দ্রুত)। আমি জিগিসিকে যখন -O2 / -O3 এ ইনলাইন করতে চাই তখন এটি প্রস্তাব করার জন্য gcc.gnu.org/bugzilla/show_bug.cgi?id=88809 একটি মন্তব্য যোগ করেছি। (16-বাইট পর্যন্ত র্যাম্প করার জন্য প্রস্তাবের সাথে যদি আমরা কেবল 4-বাইটের সাইনইনটি শুরু করতে জানি।)

GCC জানে যে এটি বাফারের জন্য 4-বাইটের সারিবদ্ধকরণ ( calloc দ্বারা নিশ্চিত), এটি GP-integer নিবন্ধকের ( -O2 এবং উচ্চতর) ব্যবহার করে 4-বাইট-এ-টাই-এ-টাইম স্কলার বিথ্যাক হিসাবে স্ট্রল ইনলাইন করতে পছন্দ করে।

(এক সময়ে 4 বাইট পড়া কেবল তখনই নিরাপদ থাকে যদি আমরা জানি যে আমরা এমন কোনও পৃষ্ঠায় ক্রস করতে পারছি না যা কোন স্ট্রিং বাইট ধারণ করে না এবং এভাবে এটি অপঠিত হতে পারে। এটির মধ্যে একটি বাফারের শেষের পূর্ববর্তীটি পড়া নিরাপদ পৃষ্ঠাটি x86 এবং x64? (টিএল: ডিআর হ্যাঁ, এএসএম এর মধ্যে, তাই কম্পাইলারগুলি কোডটি নির্বাহ করতে পারে যা সি উত্সারে এটি করলেও এটি UB হয়। libc strlen বাস্তবায়নগুলিও এর সদ্ব্যবহার করে। আমার উত্তরটি এখানে দেখুন glibc strlen লিঙ্ক এবং বড় স্ট্রিংগুলির জন্য এটি কত দ্রুত সঞ্চালিত হয় তার সারাংশ।)

At -O1 , Gcc সর্বদা (এমনকি জ্ঞাত অ্যালাইনমেন্ট ছাড়াও) repnz scasb হিসাবে repnz scasb , যা খুব ধীর (আধুনিক Intel repnz scasb প্রতি ঘড়ির চক্রের প্রতি 1 বাইট)। "দ্রুত স্ট্রিংগুলি" কেবলমাত্র rep movs repz / repnz নির্দেশাবলী নয়, দুর্ভাগ্যবশত rep stos এবং rep movs প্রযোজ্য। তাদের মাইক্রোকোডটি এক সময়ে কেবলমাত্র 1 বাইট হয় তবে তাদের এখনও কিছু স্টার্টআপ ওভারহেড থাকে। ( https://agner.org/optimize/ )

(আমরা কম্পাইলার থেকে কম্পাইলার থেকে "গোপন" s মাধ্যমে এটি একটি volatile void *tmp সংরক্ষণ / পুনরায় লোড করে পরীক্ষা করতে পারি। উদাহরণস্বরূপ, gccকে পয়েন্টার মান সম্পর্কে শূন্য অনুমান করা উচিত যা কোনও volatile থেকে ফিরে পড়া, কোনও সারিবদ্ধকরণ তথ্য ধ্বংস করা ।)

সাধারণভাবে স্ট্রিং ক্রিয়াকলাপগুলিকে rep_byte জন্য জিসিসি-তে কিছু x86 টিউনিং বিকল্প রয়েছে যেমন- -mstringop-strategy=libcall vs. -mstringop-strategy=libcall vs. rep_byte (শুধুমাত্র strlen; memcmp অন্য একটি বড় অংশ যা rep এবং লুপ দিয়ে করা যেতে পারে)। আমি এখানে এই প্রভাব কি চেক না।

অন্য বিকল্পের জন্য ডক্স বর্তমান আচরণ বর্ণনা করে। আমরা এই ইনলাইনিং (অ্যালাইনমেন্ট-হ্যান্ডলিংয়ের জন্য অতিরিক্ত কোড সহ) পেতে পারি এমনকি এমন ক্ষেত্রেও যেখানে আমরা অননুমোদিত পয়েন্টারগুলিতে এটি চেয়েছিলাম। (এটি একটি প্রকৃত পারফ জয়, বিশেষ করে ছোট স্ট্রিংগুলির জন্য, যেখানে ইনলাইন লুপ মেশিনের তুলনায় আবর্জনা ছিল না এমন লক্ষ্যগুলিতে।)

-minline-all-stringops
ডিফল্টরূপে গিসিসি শুধুমাত্র স্ট্রিং ক্রিয়াকলাপগুলিকে ইনলাইন করে যখন গন্তব্যটিকে কমপক্ষে 4-বাইট সীমানা সংলগ্ন করা হয়। এটি আরও ইনলাইনিং এবং কোড আকার বাড়ায়, তবে কোডের কর্মক্ষমতা উন্নত করতে পারে যা ক্ষুদ্র দৈর্ঘ্যের জন্য দ্রুত memcpy, strlen এবং স্মৃতিস্তম্ভের উপর নির্ভর করে।

GCC এছাড়াও প্রতি-ফাংশন বৈশিষ্ট্যগুলি ব্যবহার করে আপনি দৃশ্যত এটি নিয়ন্ত্রণ করতে ব্যবহার করতে পারেন, যেমন __attribute__((no-inline-all-stringops)) void foo() { ... } , তবে আমি এটির সাথে প্রায় খেলিনি। (যেটি ইনলাইন-এর বিপরীত। এর অর্থ ইনলাইনটি নয়, এটি কেবল তখনই ইনলাইন করা যায় যখন 4-বাইটের সংমিশ্রণটি পরিচিত হয়।)

জিইসি এর ইনলাইন strlen কৌশল উভয় 16-বাইট সারিবদ্ধতার সুবিধা নিতে ব্যর্থ হয়েছে এবং x 86-64 এর জন্য বেশ খারাপ

ছোট-স্ট্রিং কেসটি খুব সাধারণ না হওয়া পর্যন্ত, 4-বাইটের অংশে কাজ করে, তারপর 8-বাইট অংশগুলি সংযুক্ত করে 4-বাইটের মতো দ্রুত দ্বিগুণ করে।

এবং 4-বাইট কৌশলটি শূন্য বাইট ধারণকারী ড্যাড্ডের মধ্যে বাইট খোঁজার জন্য প্রয়োজনীয় তুলনায় অনেক ধীর পরিচ্ছন্নতা রয়েছে। এটি তার উচ্চ বিট সেট দিয়ে একটি বাইট সন্ধান করে এটি সনাক্ত করে, তাই এটি কেবল অন্যান্য বিট বন্ধ মাস্ক এবং bsf (বিট স্ক্যান স্ক্যান) ব্যবহার করা উচিত । আধুনিক CPUs (Intel এবং Ryzen) এর উপর 3 চক্রের বিলম্ব রয়েছে। বা কম্পাইলাররা rep bsf tzcnt ব্যবহার করতে পারে তাই এটি tzcnt হিসাবে tzcnt তে tzcnt হয় যা tzcnt সমর্থন করে, এটি AMD এ আরও কার্যকর। tzcnt এবং tzcnt অ শূন্য ইনপুট জন্য একই ফলাফল দিতে।

GCC এর 4-বাইট লুপটি বিশুদ্ধ সি, বা কিছু লক্ষ্য-স্বাধীন লজিক থেকে কম্পাইল হওয়া দেখে মনে হচ্ছে, বিটসকানের সুবিধা গ্রহণ করে না। andn ব্যবহার করে x86 এ andn সাথে কম্পাইল করার সময় এটি অপটিমাইজ করার জন্য ব্যবহার করে, তবে এটি এখনও চক্র প্রতি 4 বাইটের কম।

SSE2 pcmpeqb + pcmpeqb ছোট এবং দীর্ঘ উভয় ইনপুটগুলির জন্য অনেক বেশি ভাল । x86-64 গ্যারান্টি দেয় যে SSE2 উপলব্ধ, এবং x86-64 সিস্টেম V এর alignof(maxalign_t) = 16 তাই alignof(maxalign_t) = 16 সবসময় কমপক্ষে 16-বাইট সাইন ইন পয়েন্টারগুলি ফেরত দেবে।

আমি কর্মক্ষমতা পরীক্ষা strlen ব্লক জন্য একটি প্রতিস্থাপন লিখেছেন

প্রত্যাশিত হিসাবে এটি স্কাইলেকে প্রায় 4x দ্রুত 4 বার পরিবর্তে 16 বাইট চলছে।

(আমি আসল উৎসটিকে -O3 সাথে সংকলন -O3 , তারপর -O3 সম্পাদনা করে -O3 ইনলাইন সম্প্রসারণের জন্য এই কৌশলটির সাথে কোন পারফরমেন্স হওয়া উচিত ছিল। আমি এটি C উত্সের ভিতরে asline inline করতে পোর্ট করেছিলাম; গডবোল্টের সেই সংস্করণটি দেখুন । )

    # at this point gcc has `s` in RDX, `i` in ECX

    pxor       %xmm0, %xmm0         # zeroed vector to compare against
    .p2align 4
.Lstrlen16:                         # do {
#ifdef __AVX__
    vpcmpeqb   (%rdx), %xmm0, %xmm1
#else
    movdqa     (%rdx), %xmm1
    pcmpeqb    %xmm0, %xmm1           # xmm1 = -1 where there was a 0 in memory
#endif

    add         $16, %rdx             # ptr++
    pmovmskb  %xmm1, %eax             # extract high bit of each byte to a 16-bit mask
    test       %eax, %eax
    jz        .Lstrlen16            # }while(mask==0);
    # RDX points at the 16-byte chunk *after* the one containing the terminator
    # EAX = bit-mask of the 0 bytes, and is known to be non-zero
    bsf        %eax, %eax           # EAX = bit-index of the lowest set bit

    movb       $'A', -16(%rdx, %rax)

মনে রাখবেন আমি স্টোর অ্যাড্রেসিং মোডে স্ট্র্লেন ক্লিনআপের অংশটি অপ্টিমাইজ করেছি: -16 ডিসপ্লেসমেন্টের মাধ্যমে আমি ওভারহুটের জন্য সঠিক, এবং এটি কেবল স্ট্রিংটির শেষটি খুঁজে পাচ্ছে, আসলে দৈর্ঘ্য গণনা করে না এবং তারপর GCC এর মতো সূচী ইতিমধ্যেই ছিল তার 4-বাইট-এ-একটি-সময় লুপ inlining পরে করছেন।

প্রকৃত স্ট্রিং দৈর্ঘ্য (পয়েন্টারের পরিবর্তে শেষের দিকে) পেতে, আপনি RX-শটটি কমিয়ে আনুন এবং তারপরে rax-16 যোগ করুন (সম্ভবত একটি এলইএর সাথে 2 নিবন্ধক যোগ করতে + একটি ধ্রুবক, কিন্তু 3-উপাদানের এলইএ বেশি বিলম্বিত থাকে।)

AVX লোডের অনুমতি সহ + শূন্য নিবন্ধনের বিনাশ ছাড়াই একটি নির্দেশনায় তুলনা করুন, সমগ্র লুপটি কেবলমাত্র 5 টি ইউপস, 5 থেকে নিচে। (পরীক্ষা / জেজ ম্যাক্রো- vpcmpeqb একটি ইনডেক্স এবং এএমডি উভয় vpcmpeqb একটি vpcmpeqbvpcmpeqb অ-সূচীযুক্ত মেমরি-উৎসটি পুরো পাইপলাইনের মাধ্যমে মাইক্রো-ফিউজড রাখতে পারে, তাই এটি ফ্রন্ট-এন্ডের জন্য শুধুমাত্র 1 টি সংযুক্ত ডোমেইন।

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

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

খুব ছোট স্ট্রিং ক্ষেত্রে (0 থেকে 3 বাইটের মতো), বিশুদ্ধ স্কলার (বিশেষ করে বুলডোজার-পরিবারে) ব্যবহার করে সামান্য কম প্রবণতা অর্জন করা সম্ভব হলেও 0 থেকে 15 বাইটের সমস্ত স্ট্রিংগুলি গ্রহণ করা একই শাখা পথ (লুপ শাখা কখনও নেওয়া হয়নি) সবচেয়ে ছোট স্ট্রিং ব্যবহার-ক্ষেত্রে জন্য খুব সুন্দর

15 বাইট পর্যন্ত সমস্ত স্ট্রিংগুলির জন্য খুব ভালো হওয়া ভালো পছন্দ হিসাবে মনে হয়, যখন আমরা জানি যে আমাদের 16-বাইটের সমন্বয় রয়েছে। আরো প্রত্যাশিত শাখা খুব ভাল। (এবং নোট করুন যে যখন লুপিং হয়, তখন pmovmskb প্রবণতাটি কেবল তখনই প্রভাবিত হয় যে আমরা লুপটি ভেঙে শাখা ভুল প্রমানগুলি সনাক্ত করতে পারি; শাখা ভবিষ্যদ্বাণী + ফটকাবাজি কার্যকরকরণ প্রতিটি পুনরাবৃত্তিতে স্বাধীন pmovmskb এর বিলম্বিততা লুকিয়ে রাখে।

যদি আমরা দীর্ঘ স্ট্রিংগুলিকে সাধারণ বলে মনে করি তবে আমরা কিছুটা আনলল করতে পারি, তবে সেই মুহুর্তে আপনাকে libc ফাংশনটি কল করা উচিত যাতে এটি রানটাইমতে AVX2 এ পাঠাতে পারে। 1 টিরও বেশি ভেক্টরকে আনলোল করা সহজ সমাধানগুলিকে আঘাত করে ক্লিনআপটি জটিল করে।

আমার মেশিন i7-6700k স্কিলকে 4.2GHz সর্বোচ্চ টার্বোতে (এবং energy_performance_preference = পারফরম্যান্স), আর্ক লিনাক্সে gcc8.2 দিয়ে, আমি কিছু সামঞ্জস্যপূর্ণ বেঞ্চমার্ক টাইমিং পেয়েছি কারণ আমার CPU ঘড়ি গতি স্মৃতি সময় র্যাম্প আপ হয়। কিন্তু সর্বদা সর্বাধিক টার্বো না; মেমরি আবদ্ধ যখন Skylake এর hw শক্তি ব্যবস্থাপনা downclocks। স্টাফ আউটপুট গড়তে এবং stderr এ perf সারাংশ দেখতে যখন এটি perf stat দেখানো আমি সাধারণত কাছাকাছি 4.0GHz অধিকার পেয়েছি।

perf stat -r 100 ./a.out | awk '{sum+= $1}  END{print sum/100;}'

আমি আমার ASM একটি GNU C inline-ASM বিবৃতিতে অনুলিপি শেষ করেছি, তাই আমি কোডটি গডবোল্ট কম্পাইলার এক্সপ্লোরারে রাখতে পারি

বড় স্ট্রিংয়ের জন্য, প্রশ্নটির মতো দৈর্ঘ্য: ~ 4GHz স্কাইল্যাকের সময়

  • ~ 62100 clock_t সময় ইউনিট: -O1 rep scas: ( clock() একটু অপ্রচলিত, তবে আমি এটি পরিবর্তন করতে বিরক্ত না।)
  • ~ 15900 clock_t সময় ইউনিট: -O3 3 -O3 4-বাইট লুপ কৌশল: 100 রান = গড়। (অথবা হয়তো ~ 15800 -march=native এবং andn for andn )
  • ~ 1880 clock_t সময় ইউনিট: -O3 glibc -O3 ফাংশন কল, AVX2 ব্যবহার করে
  • ~ 3190 clock_t সময় ইউনিট: (AVX1 128-বিট ভেক্টর, 4 ইউপ লুপ) হ্যান্ড লিখিত ইনলাইন ASM যে gcc / ইনলাইন করা উচিত।
  • ~ 3230 clock_t সময় ইউনিট: (এসএসই 2 5 ইউপ লুপ) জি-সি-র ইনলাইন / ইনলাইন হওয়া উচিত।

আমার হাতে লেখা ASM সংক্ষিপ্ত স্ট্রিংগুলির জন্য খুব ভাল হওয়া উচিত, কারণ এটি বিশেষভাবে শাখা করার প্রয়োজন হয় না। পরিচিত অ্যালাইনমেন্ট strlen জন্য খুব ভাল, এবং libc এটির সুবিধা নিতে পারে না।

আমরা বড় স্ট্রিং বিরল হতে আশা করি, যে ক্ষেত্রে জন্য libc চেয়ে 1.7x ধীর। 1 এম বাইটের দৈর্ঘ্য মানে এটি আমার CPU এ L2 (256k) বা L1d ক্যাশে (32k) তে গরম থাকতে পারে না, তাই L3 ক্যাশে এমনকি আটকে থাকাও libc সংস্করণ দ্রুত ছিল। (সম্ভবত একটি অরোল্লড লুপ এবং 256-বিট ভেক্টর ROB কে বাইট হিসাবে অনেকগুলি ইউপস দিয়ে আটকাতে পারে না, তাই OOO exec আরও এগিয়ে দেখতে পারে এবং আরও মেমরি সমান্তরাল পেতে পারে, বিশেষ করে পৃষ্ঠা সীমানাগুলিতে।)

কিন্তু L3 ক্যাশে ব্যান্ডউইথটি সম্ভবত 4-ইওপ সংস্করণটি প্রতি ঘন্টায় 1 পুনরাবৃত্তি থেকে রুপান্তরিত হওয়া একটি বন্ধকী, সুতরাং আমরা লুপে AVX সংরক্ষণ করে আমাদের কম সুবিধা দেখছি। L1d ক্যাশে গরম ডেটা সহ, আমাদের প্রতি পুনরাবৃত্তি প্রতি 1.25 চক্র পেতে হবে 1।

কিন্তু একটি ভাল AVX2 বাস্তবায়ন zeros চেক করার আগে জোড়া জোড়া এবং vpminub ব্যবহার করে 64 বাইট প্রতি চক্র (2x 32 বাইট লোড) vpminub পারে যেখানে তারা ছিল যেখানে ফিরে যেতে। এই এবং libc মধ্যে ফাঁক ~ 2 কে থেকে ~ 30 কিউবি আকারের জন্য বৃহত্তর খোলা বা যাতে L1d মধ্যে গরম থাকুন।

দৈর্ঘ্য = 1000 এর সাথে কেবলমাত্র কয়েকটি পঠনযোগ্য পরীক্ষার ইঙ্গিত দেয় যে L1d ক্যাশে গরম মাঝারি আকারের স্ট্রিংগুলির জন্য আমার লুপের চেয়ে গ্লিবিসি স্ট্রলন প্রায় 4x দ্রুত । AVX2 এর জন্য বড় অরোল্লড লুপ পর্যন্ত ঢোকানো যথেষ্ট, তবে এখনও সহজেই L1d ক্যাশে ফিট করে। (কেবলমাত্র স্টোর-ফরওয়ার্ডিং স্টলগুলিকে এড়ানোর জন্য, এবং তাই আমরা অনেক পুনরাবৃত্তি করতে পারি)

যদি আপনার স্ট্রিংগুলি বড় হয় তবে আপনাকে strlen প্রয়োজনে স্পষ্ট-দৈর্ঘ্যের স্ট্রিংগুলি ব্যবহার করা উচিত, তাই একটি সরল লুপকে ইনলাইন করা একটি যুক্তিসঙ্গত কৌশল হিসাবে মনে হয়, যতক্ষণ না এটি স্বল্প স্ট্রিংগুলির জন্য ভাল এবং মাঝারি জন্য মোট আবর্জনা নয় (300 বাইট মত) এবং খুব দীর্ঘ (> ক্যাশের আকার) স্ট্রিং।

এই সঙ্গে ছোট স্ট্রিং Benchmarking:

আমি প্রত্যাশিত ফলাফল পেতে চেষ্টা করার কিছু অদ্ভুততা মধ্যে দৌড়ে:

আমি প্রতিটি পুনরাবৃত্তি আগে স্ট্রিং truncate s[31] = 0 চেষ্টা করেছেন (সংক্ষিপ্ত ধ্রুবক দৈর্ঘ্য অনুমতি)। কিন্তু তারপর আমার এসএসই 2 সংস্করণটি জিसीसीের সংস্করণ হিসাবে প্রায় একই গতিতে ছিল। স্টোরে-ফরওয়ার্ডিং স্টলগুলি হুমকি ছিল! একটি বৃহত লোড অনুসরণ করে একটি বাইট স্টোরটি স্টোর-ফরওয়ার্ডিংটি ধীর পথটি নেয় যা L1d ক্যাশে থেকে বাইটগুলি স্টোরের বাফার থেকে বাইটগুলিকে একত্র করে। পরবর্তী অতিরিক্ত পুনরাবৃত্তির জন্য স্টোর সূচীটি গণনা করার জন্য এই অতিরিক্ত বিলম্বটি স্ট্রিটের শেষ 4-বাইট বা 16-বাইট অংশে একটি লুপ-বাহিত ডিপ শৃঙ্খলের অংশ।

GCC এর ধীরে ধীরে 4-বাইট-এ-এ-টাইম কোডটি সেই বিলম্বের ছায়ায় পূর্ববর্তী 4-বাইট অংশগুলি প্রক্রিয়াকরণ করে রাখতে পারে। (অর্ডার অফ আউট অর্ডার কার্যকর চমত্কার: ধীর কোড কখনও কখনও আপনার প্রোগ্রাম সামগ্রিক গতি প্রভাবিত করতে পারে না)।

আমি অবশেষে এটি কেবল একটি পঠনযোগ্য সংস্করণ তৈরি করে সমাধান করেছি এবং লুপের বাইরে strlen উত্তোলন থেকে কম্পাইলারকে থামাতে ইনলাইন ASM ব্যবহার করে।

কিন্তু স্টোর-ফরওয়ার্ডিং 16-বাইট লোড ব্যবহার করে একটি সম্ভাব্য সমস্যা। যদি অন্যান্য সি ভেরিয়েবলগুলি অ্যারের শেষে সংরক্ষণ করা হয়, তবে আমরা সংকোচকারী দোকানে তুলনায় অ্যারের শেষে লোড হওয়ার কারণে একটি SF স্টল আঘাত করতে পারি। সম্প্রতি-অনুলিপিযুক্ত তথ্যের জন্য, আমরা 16-বাইট বা বৃহত্তর সংলগ্ন স্টোরগুলির সাথে অনুলিপি করলে আমরা ঠিক আছি, কিন্তু ছোট কপিগুলির জন্য গ্লিবসি মেমস্কি 2x ওভারল্যাপিং লোডগুলি যা বস্তুর শুরু এবং শেষ থেকে সমগ্র বস্তুর আচ্ছাদন করে। তারপরে এটি আবার সংরক্ষণ করা হয়, memmove src overlaps dst ক্ষেত্রে পরিচালনা করা বিনামূল্যে। সুতরাং একটি ছোট স্ট্রিংয়ের দ্বিতীয় 16-বাইট বা 8-বাইটের অংশটি কেবলমাত্র স্মৃতিচিহ্নযুক্ত ছিল তা আমাদের শেষ অংশটি পড়ার জন্য একটি এসএফ স্টল সরবরাহ করতে পারে। (আউটপুট জন্য তথ্য নির্ভরতা আছে যে এক।)

শুধু ধীর গতির তাই আপনি এটি প্রস্তুত হওয়ার আগে শেষ পর্যন্ত পাবেন না সাধারণত সাধারণ না, তাই এখানে কোন দুর্দান্ত সমাধান নেই। আমার মনে হয় বেশিরভাগ সময় আপনি যা লিখেছেন তা বাফার করতে যাচ্ছেন না, সাধারণত আপনি কেবল একটি ইনপুট strlen করতে যাচ্ছেন যা আপনি কেবল পড়ছেন তাই স্টোর-ফরওয়ার্ডিং স্টলগুলি কোনও সমস্যা নয় । অন্য কিছু যদি শুধু এটি লিখেছে, তাহলে কার্যকরী কোডটি আশা করে দৈর্ঘ্যটি নিক্ষেপ করে নি এবং কোনও ফাংশনটিকে পুনঃসংখ্যাবদ্ধ করে বলে।

অন্য অদ্ভুততা আমি সম্পূর্ণরূপে figured আছে না:

কোড সংমিশ্রণ শুধুমাত্র পাঠযোগ্য, আকার = 1000 ( s[1000] = 0; ) এর জন্য 2 পার্থক্যের একটি ফ্যাক্টর তৈরি করছে। কিন্তু অভ্যন্তরীণ-সর্বাধিক .p2align 4 লুপ নিজেই .p2align 4 বা .p2align 5 সাথে সংযুক্ত করা হয়। লুপ সংমিশ্রণ বৃদ্ধি 2 এটি একটি ফ্যাক্টর দ্বারা ধীর করতে পারেন!

# slow version, with *no* extra HIDE_ALIGNMENT function call before the loop.
# using my hand-written asm, AVX version.
  i<1280000 read-only at strlen(s)=1000 so strlen time dominates the total runtime (not startup overhead)
  .p2align 5 in the asm inner loop. (32-byte code alignment with NOP padding)

gcc -DUSE_ASM -DREAD_ONLY -DHIDE_ALIGNMENT -march=native -O3 -g strlen-microbench.c &&
 time taskset -c 3 perf stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,branches,branch-misses,instructions,uops_issued.any,uops_executed.thread -r 100 ./a.out |
 awk '{sum+= $1}  END{print sum/100;}'

 Performance counter stats for './a.out' (100 runs):

             40.92 msec task-clock                #    0.996 CPUs utilized            ( +-  0.20% )
                 2      context-switches          #    0.052 K/sec                    ( +-  3.31% )
                 0      cpu-migrations            #    0.000 K/sec                  
               313      page-faults               #    0.008 M/sec                    ( +-  0.05% )
       168,103,223      cycles                    #    4.108 GHz                      ( +-  0.20% )
        82,293,840      branches                  # 2011.269 M/sec                    ( +-  0.00% )
         1,845,647      branch-misses             #    2.24% of all branches          ( +-  0.74% )
       412,769,788      instructions              #    2.46  insn per cycle           ( +-  0.00% )
       466,515,986      uops_issued.any           # 11401.694 M/sec                   ( +-  0.22% )
       487,011,558      uops_executed.thread      # 11902.607 M/sec                   ( +-  0.13% )

         0.0410624 +- 0.0000837 seconds time elapsed  ( +-  0.20% )

40326.5   (clock_t)

real    0m4.301s
user    0m4.050s
sys     0m0.224s

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

সম্ভবত অভ্যন্তরীণ এবং বাইরের লুপ শাখা একে অপরের aliasing হয়, না।

নির্দেশ গণনাটি প্রায় একই রকম, অভ্যন্তরীণ লুপের বাইরের লুপে কিছু NOPs দ্বারা আলাদা। কিন্তু আইপিসি বেশ ভিন্ন: সমস্যা ছাড়া, দ্রুত সংস্করণ প্রতি প্রোগ্রামের জন্য প্রতি ঘন্টায় 4.82 নির্দেশাবলী চালায়। (এর মধ্যে বেশিরভাগই অভ্যন্তরীণ-সর্বাধিক লুপে প্রতি চক্রের 5 টি নির্দেশনা চলছে, একটি পরীক্ষা / জেজের জন্য ধন্যবাদ যা ম্যাক্রো-ফিউসকে ২ টি নির্দেশাবলীতে 1 টি নির্দেশনা দেয়।) এবং লক্ষ্য করুন যে uops_executedটি uops_issued এর চেয়ে অনেক বেশী: এর মানে মাইক্রো-ফিউশন ভাল শেষ বোতল মাধ্যমে আরো uops পেতে ভাল কাজ।

fast version, same read-only strlen(s)=1000 repeated 1280000 times

gcc -DUSE_ASM -DREAD_ONLY -UHIDE_ALIGNMENT -march=native -O3 -g strlen-microbench.c &&
 time taskset -c 3 perf stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,branches,branch-misses,instructions,uops_issued.any,uops_executed.thread -r 100 ./a.out |
 awk '{sum+= $1}  END{print sum/100;}' 

 Performance counter stats for './a.out' (100 runs):

             21.06 msec task-clock                #    0.994 CPUs utilized            ( +-  0.10% )
                 1      context-switches          #    0.056 K/sec                    ( +-  5.30% )
                 0      cpu-migrations            #    0.000 K/sec                  
               313      page-faults               #    0.015 M/sec                    ( +-  0.04% )
        86,239,943      cycles                    #    4.094 GHz                      ( +-  0.02% )
        82,285,261      branches                  # 3906.682 M/sec                    ( +-  0.00% )
            17,645      branch-misses             #    0.02% of all branches          ( +-  0.15% )
       415,286,425      instructions              #    4.82  insn per cycle           ( +-  0.00% )
       335,057,379      uops_issued.any           # 15907.619 M/sec                   ( +-  0.00% )
       409,255,762      uops_executed.thread      # 19430.358 M/sec                   ( +-  0.00% )

         0.0211944 +- 0.0000221 seconds time elapsed  ( +-  0.10% )

20504  (clock_t)

real    0m2.309s
user    0m2.085s
sys     0m0.203s

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

পরিবর্তন .p2align 5 থেকে .p2align 4 তাদের বিপরীত করে: -UHIDE_ALIGNMENT ধীর হয়ে যায়।

এই গডবোল্ট বাইনারি লিংকটি একই প্যাডিংকে পুনরুত্পাদন করে যা আমি গার্ক 8.2.1-এ আর্ক লিনাক্সে উভয় ক্ষেত্রেই nopw : 2x 11-বাইট nopw + দ্রুত ক্ষেত্রে জন্য বাইরের লুপের ভিতরে 3-বাইট nop । এটা স্থানীয়ভাবে ব্যবহার করা হয় সঠিক উৎস আছে।

ছোট strlen শুধুমাত্র-পড়া মাইক্রো-benchmarks:

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

strlen=33 , তাই টারমিনেটর 3 য় 16-বাইট ভেক্টরের শুরুতে। (আমার সংস্করণটি যতটা সম্ভব 4 বাইট সংস্করণ বনাম খারাপ হিসাবে -DREAD_ONLY ।) -DREAD_ONLY , এবং i<1280000 outer-loop পুনরাবৃত্তি লুপ হিসাবে i<1280000

  • 1933 clock_t: আমার asm : চমৎকার এবং সামঞ্জস্যপূর্ণ সেরা-কেস সময় (গড় -DHIDE_ALIGNMENT / বাউন্স করা হয় না) সমান পারফ সঙ্গে / ছাড়া -DHIDE_ALIGNMENT , দীর্ঘ -DHIDE_ALIGNMENT বিপরীতে। লুপ শাখা যে অনেক ছোট প্যাটার্ন সঙ্গে আরো সহজে predictable হয়। (strlen = 33, না 1000)।
  • 3220 clock_t: gcc-o3 strlen । ( -DHIDE_ALIGNMENT )
  • 6100 clock_t: gcc-o3 4-বাইট লুপ
  • 37200 clock_t: gcc-o1 repz scasb

তাই ছোট স্ট্রিংগুলির জন্য, আমার সহজ ইনলাইন লুপটি PLL (কল + jmp [mem] ) এর মাধ্যমে স্ট্র্লেনে যাওয়ার জন্য একটি লাইব্রেরী ফাংশন কলকে আঘাত করে, তারপর স্ট্রলেনের স্টার্টআপ ওভারহেড চালায় যা সারিবদ্ধকরণের উপর নির্ভর করে না।

strlen(s)=33 সঙ্গে সব সংস্করণ জন্য 0.05% মত নগদ শাখা ভুল misprredicts ছিল। রেপজ স্ক্যাস সংস্করণটি 0.46% ছিল, তবে এটি মোট শাখার বাইরে ছিল। অনেক অভ্যন্তরীণ লুপ অনেক সঠিকভাবে ভবিষ্যদ্বাণীপূর্ণ শাখা র্যাক আপ।

শাখা পূর্বাভাসদাতা এবং কোড-ক্যাশে গরমের সাথে, 33-বাইট স্ট্রিংয়ের জন্য repz scasb চেয়ে repz scasb 10x এরও বেশি খারাপ। এটি বাস্তব ব্যবহারের ক্ষেত্রে কম খারাপ যেখানে স্ট্রেন মিস শাখা বা এমনকি কোড-ক্যাশে এবং স্টল মিস করতে পারে, কিন্তু সরাসরি লাইন repz scasb হবে না। কিন্তু 10x বিশাল, এবং এটি একটি মোটামুটি ছোট স্ট্রিং জন্য।





glibc