240 বা ততোধিক উপাদানগুলির সাথে কোনও অ্যারে লুপ করার সময় কেন একটি বৃহত পারফরম্যান্স প্রভাব রয়েছে?


230

মরিচায় একটি অ্যারের উপরে যোগফল লুপ চালানোর সময়, আমি CAPACITY> = 240. CAPACITY= 239 প্রায় 80 গুণ বেশি গতিতে দেখলাম তখন একটি বিশাল পারফরম্যান্স ড্রপ লক্ষ্য করেছি ।

"সংক্ষিপ্ত" অ্যারেগুলির জন্য কোনও বিশেষ সংকলন অপ্টিমাইজেশন জাস্ট কাজ করছে?

সঙ্গে সংকলিত rustc -C opt-level=3

use std::time::Instant;

const CAPACITY: usize = 240;
const IN_LOOPS: usize = 500000;

fn main() {
    let mut arr = [0; CAPACITY];
    for i in 0..CAPACITY {
        arr[i] = i;
    }
    let mut sum = 0;
    let now = Instant::now();
    for _ in 0..IN_LOOPS {
        let mut s = 0;
        for i in 0..arr.len() {
            s += arr[i];
        }
        sum += s;
    }
    println!("sum:{} time:{:?}", sum, now.elapsed());
}


4
240 দিয়ে আপনি কোনও সিপিইউ ক্যাশে লাইন উপচে পড়ছেন? যদি এটি হয় তবে আপনার ফলাফলগুলি খুব সিপিইউ নির্দিষ্ট।
রডরিগো

11
এখানে পুনরুত্পাদন । এখন আমি অনুমান করছি যে এটি লুপ আন্রোলিংয়ের সাথে কিছু করার আছে।
রডরিগো

উত্তর:


355

সংক্ষিপ্তসার : 240 এর নীচে, এলএলভিএম পুরোপুরি অভ্যন্তরীণ লুপটিকে তালিকাভুক্ত করে এবং এটি আপনাকে লক্ষ্য করতে দেয় যে এটি আপনার মানদণ্ডকে ভেঙে পুনরাবৃত্তি লুপটিকে অপ্টিমাইজ করতে পারে।



আপনি একটি যাদু থ্রেশহোল্ড পেয়েছেন যার উপরে এলএলভিএম নির্দিষ্ট অপ্টিমাইজেশান সম্পাদন বন্ধ করে দিয়েছে । প্রান্তিকতা 8 বাইট * 240 = 1920 বাইট (আপনার অ্যারে usizeগুলি এর একটি অ্যারে , তাই দৈর্ঘ্যটি 8 বাইট দ্বারা গুন করা হবে, x86-64 সিপিইউ ধরে নেওয়া হবে)। এই মানদণ্ডে, একটি নির্দিষ্ট অপ্টিমাইজেশন - কেবল দৈর্ঘ্যের 239 এর জন্য সম্পাদিত - বিশাল গতির পার্থক্যের জন্য দায়ী। তবে আসুন আস্তে আস্তে শুরু করুন:

(এই উত্তরের সমস্ত কোডই সংকলিত -C opt-level=3)

pub fn foo() -> usize {
    let arr = [0; 240];
    let mut s = 0;
    for i in 0..arr.len() {
        s += arr[i];
    }
    s
}

এই সাধারণ কোডটি মোটামুটিভাবে সমাবেশটি তৈরি করতে পারে যার আশা করা যায়: একটি লুপ যোগ করার উপাদান। তবে, আপনি যদি পরিবর্তন 240করেন 239তবে নির্গত সমাবেশটি অনেকটা আলাদা হয়। গডবোল্ট কম্পাইলার এক্সপ্লোরারে এটি দেখুন । এখানে সমাবেশের একটি ছোট অংশ রয়েছে:

movdqa  xmm1, xmmword ptr [rsp + 32]
movdqa  xmm0, xmmword ptr [rsp + 48]
paddq   xmm1, xmmword ptr [rsp]
paddq   xmm0, xmmword ptr [rsp + 16]
paddq   xmm1, xmmword ptr [rsp + 64]
; more stuff omitted here ...
paddq   xmm0, xmmword ptr [rsp + 1840]
paddq   xmm1, xmmword ptr [rsp + 1856]
paddq   xmm0, xmmword ptr [rsp + 1872]
paddq   xmm0, xmm1
pshufd  xmm1, xmm0, 78
paddq   xmm1, xmm0

এটিকে বলা হয় লুপ আন্রোলিং : এলএলভিএম সেই সমস্ত "লুপ পরিচালন নির্দেশাবলী" চালানো এড়াতে লুপের বডিটিকে অনেক সময় ব্যয় করে, লুপের পরিবর্তনশীল বৃদ্ধি করে, লুপটি শেষ হয়েছে কিনা এবং লুপের শুরুতে লাফটি পরীক্ষা করে দেখুন? ।

আপনি যদি ভাবছেন: paddqএকই এবং অনুরূপ নির্দেশাবলী হ'ল সিমডি নির্দেশনা যা সমান্তরালভাবে একাধিক মান সংযোজন করতে দেয়। তদুপরি, দুটি 16-বাইট সিমডি রেজিস্টারগুলি ( xmm0এবং xmm1) সমান্তরালভাবে ব্যবহৃত হয় যাতে সিপিইউয়ের নির্দেশ-স্তরের সমান্তরালতা মূলত একই সাথে দুটি নির্দেশকে কার্যকর করতে পারে। সর্বোপরি, তারা একে অপরের থেকে স্বাধীন। শেষ পর্যন্ত, উভয় নিবন্ধগুলি একসাথে যুক্ত করা হয় এবং তারপরে অনুভূমিকভাবে স্কেলারের ফলাফলের সংক্ষিপ্তসার করা হয়।

আধুনিক মূলধারার x86 সিপিইউগুলি (লো-পাওয়ার অ্যাটম নয়) তারা এল 1 ডি ক্যাশে আঘাত করলে ঘড়ি প্রতি 2 ভেক্টর বোঝা সত্যিই করতে পারে এবং paddqবেশিরভাগ সিপিইউতে 1 চক্রের বিলম্বের সাথে থ্রুপুটও প্রতি ঘড়িতে কমপক্ষে 2 হয়। Https://agner.org/optimize/ এবং এই প্রশ্নোত্তরও দেখুন একাধিক আহরণকারীকে (কোনও ডট পণ্যের জন্য এফপি এফএমএর) আড়াল করতে এবং এর পরিবর্তে থ্রুটপটে বাধা পেতে ।

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


তবে লুপ আন্রোলিং 80 এর গুণমানের পারফরম্যান্সের জন্য দায়ী নয়! অন্তত একা অনিয়ন্ত্রিত লুপ না। আসুন আসল বেঞ্চমার্কিং কোডটি একবার দেখুন, যা একটি লুপটিকে অন্য একটিটির ভিতরে রাখে:

const CAPACITY: usize = 239;
const IN_LOOPS: usize = 500000;

pub fn foo() -> usize {
    let mut arr = [0; CAPACITY];
    for i in 0..CAPACITY {
        arr[i] = i;
    }

    let mut sum = 0;
    for _ in 0..IN_LOOPS {
        let mut s = 0;
        for i in 0..arr.len() {
            s += arr[i];
        }
        sum += s;
    }

    sum
}

( গডবোল্ট কম্পাইলার এক্সপ্লোরার অন )

জন্য সমাবেশ CAPACITY = 240 দুই নেস্টেড loops: স্বাভাবিক সৌন্দর্য। (ফাংশনটির শুরুতে কেবল কিছু শুরু করার জন্য বেশ কয়েকটি কোড রয়েছে, যা আমরা উপেক্ষা করব)) 239 এর জন্য, এটি দেখতে খুব আলাদা দেখাচ্ছে! আমরা দেখতে পাই যে প্রারম্ভকৃত লুপ এবং অভ্যন্তরীণ লুপটি নিবন্ধবিহীন হয়ে গেছে: এখনও পর্যন্ত প্রত্যাশিত।

গুরুত্বপূর্ণ পার্থক্যটি হল যে 239 এর জন্য, এলএলভিএম এটি নির্ধারণ করতে সক্ষম হয়েছিল যে অভ্যন্তরীণ লুপের ফলাফলটি বাইরের লুপের উপর নির্ভর করে না!ফলস্বরূপ, এলএলভিএম কোডটি নির্গত করে যা মূলত প্রথমে কেবলমাত্র অভ্যন্তরীণ লুপটি নির্বাহ করে (সমষ্টি গণনা করে) এবং তারপরে যোগ করে বাইরের লুপটি অনুকরণ করেsum একগুচ্ছ সময় !

প্রথমে আমরা উপরের মতো প্রায় একই সমাবেশটি দেখি (সমাবেশটি অভ্যন্তরীণ লুপকে উপস্থাপন করে)। এরপরে আমরা এটি দেখতে পেলাম (সমাবেশটি ব্যাখ্যা করার জন্য আমি মন্তব্য করেছি; যার সাথে মন্তব্যগুলি *বিশেষভাবে গুরুত্বপূর্ণ):

        ; at the start of the function, `rbx` was set to 0

        movq    rax, xmm1     ; result of SIMD summing up stored in `rax`
        add     rax, 711      ; add up missing terms from loop unrolling
        mov     ecx, 500000   ; * init loop variable outer loop
.LBB0_1:
        add     rbx, rax      ; * rbx += rax
        add     rcx, -1       ; * decrement loop variable
        jne     .LBB0_1       ; * if loop variable != 0 jump to LBB0_1
        mov     rax, rbx      ; move rbx (the sum) back to rax
        ; two unimportant instructions omitted
        ret                   ; the return value is stored in `rax`

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

এর অর্থ রানটাইম থেকে পরিবর্তিত CAPACITY * IN_LOOPSহয়CAPACITY + IN_LOOPS । এবং এটি বিশাল পারফরম্যান্সের পার্থক্যের জন্য দায়ী।


একটি অতিরিক্ত নোট: আপনি এই সম্পর্কে কিছু করতে পারেন? আসলে তা না. এলএলভিএম এর যেমন জাদু থ্রেশহোল্ড থাকতে হবে সেগুলি ছাড়া এলএলভিএম-অপটিমাইজেশন নির্দিষ্ট কোডে সম্পূর্ণ হতে চিরতরে নিতে পারে। তবে আমরা সম্মত হতে পারি যে এই কোডটি অত্যন্ত কৃত্রিম ছিল। বাস্তবে, আমি সন্দেহ করি যে এত বড় পার্থক্য ঘটবে। সম্পূর্ণ লুপ আন্রোলিংয়ের কারণে পার্থক্য সাধারণত এই ক্ষেত্রে ফ্যাক্টর 2 হয় না। সুতরাং আসল ব্যবহারের ক্ষেত্রে চিন্তা করার দরকার নেই।

আইডোমেটিক জাস্ট কোড সম্পর্কে একটি সর্বশেষ নোট হিসাবে: arr.iter().sum()একটি অ্যারের সমস্ত উপাদানগুলি যোগ করার একটি ভাল উপায়। এবং দ্বিতীয় উদাহরণে এটি পরিবর্তন করার ফলে নির্গত সমাবেশে কোনও উল্লেখযোগ্য পার্থক্য দেখা যায় না। আপনি সংক্ষিপ্ত এবং আইডোমেটিক সংস্করণগুলি ব্যবহার করা উচিত যদি না আপনি পরিমাপ করেন যে এটির কার্যকারিতা ব্যথা করে।


2
@ লুকাশ-কালবার্টট দুর্দান্ত উত্তরের জন্য ধন্যবাদ! এখন আমি আরও বুঝতে পারি যে sumকোনও স্থানীয় না হয়ে সরাসরি আপডেট হওয়া মূল কোডটি কেন sখুব ধীরে চলছিল। for i in 0..arr.len() { sum += arr[i]; }
গাই করল্যান্ড

4
@ লুকাসক্যালবার্টোড্ট এলএলভিএম-তে আর কিছু চলছে যা অ্যাভিএক্স 2 চালু করে এমনটি করা উচিত নয়। মরিচায়ও প্রতিক্রিয়া
জানালেন

4
@ মেটেজ আকর্ষণীয়! তবে এই প্রান্তিকতা উপলব্ধ সিমডি নির্দেশাবলীর উপর নির্ভরশীল করে তোলা আমার পক্ষে খুব বেশি পাগল বলে মনে হচ্ছে না, কারণ এটি শেষ পর্যন্ত সম্পূর্ণ নিয়ন্ত্রিত লুপের নির্দেশের সংখ্যা নির্ধারণ করে। তবে দুর্ভাগ্যক্রমে, আমি নিশ্চিত করে বলতে পারি না। এটির উত্তর দিয়ে একটি এলএলভিএম দেব খুশি হবেন।
লুকাস কালবার্তোড্ট

7
সংকলক বা এলএলভিএম কেন বুঝতে পারে না যে সম্পূর্ণ গণনাটি সংকলন সময়ে করা যায়? আমি লুপ ফলাফল হার্ডকোডযুক্ত হতে পারে আশা করি। নাকি তা Instantআটকাতে ব্যবহার হচ্ছে ?
অপ্রচলিত নাম

4
@ জোসেফগারভিন: আমি ধরে নিয়েছি কারণ এটি সম্পূর্ণরূপে তালিকাভুক্তি হ'ল পরে অপ্টিমাইজেশানটি এটি দেখতে দেয়। মনে রাখবেন যে অপ্টিমাইজ করা সংকলকগুলি এখনও দ্রুত সংকলন করার পাশাপাশি যত্নবান অ্যাসেম তৈরির বিষয়ে যত্নশীল, তাই তারা যে কোনও বিশ্লেষণের সবচেয়ে খারাপ-জটিল জটিলতা সীমাবদ্ধ করতে হবে তাই জটিল লুপগুলি সহ কিছু বাজে উত্স কোডটি সংকলন করতে ঘন্টা / দিন সময় লাগে না doesn't । তবে হ্যাঁ, এটি স্পষ্টতই আকার> = 240 এর জন্য একটি মিস অপটিমাইজেশন I লুপের অভ্যন্তরে লুপগুলি অপ্টিমাইজ করা না করা যদি সাধারণ বেঞ্চমার্কগুলি না ভাঙা ইচ্ছাকৃত হয়? সম্ভবত না, তবে হতে পারে।
পিটার

30

লুকাসের উত্তর ছাড়াও, আপনি যদি পুনরুক্তি ব্যবহার করতে চান তবে এটি চেষ্টা করুন:

const CAPACITY: usize = 240;
const IN_LOOPS: usize = 500000;

pub fn bar() -> usize {
    (0..CAPACITY).sum::<usize>() * IN_LOOPS
}

পরিসীমা প্যাটার্ন সম্পর্কে পরামর্শের জন্য @ ক্রিস মরগানকে ধন্যবাদ।

সমাবেশ অপ্টিমাইজ বেশ ভাল হল:

example::bar:
        movabs  rax, 14340000000
        ret

3
বা আরও ভাল এখনও, (0..CAPACITY).sum::<usize>() * IN_LOOPSযা একই ফলাফল দেয়।
ক্রিস মরগান

11
আমি প্রকৃতপক্ষে ব্যাখ্যা করব যে সমাবেশটি আসলে গণনা করছে না, তবে এলএলভিএম এই ক্ষেত্রে উত্তরটি পূর্বসূর করে দিয়েছে।
জোসেপ

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

1
@ ডেভিস্লোর: volatileসেই স্মৃতিটিকে সিঙ্ক করতে বাধ্য করে। এটি লুপ কাউন্টারে প্রয়োগ করা কেবল লুপের পাল্টা মানটির প্রকৃত পুনরায় লোড / স্টোরকে বাধ্য করে। এটি সরাসরি লুপের শরীরকে প্রভাবিত করে না। এজন্য এটি ব্যবহারের আরও ভাল উপায় হ'ল প্রকৃত গুরুত্বপূর্ণ ফলাফলটি volatile int sinkলুপের পরে (যদি একটি লুপ বহনকারী নির্ভরতা থাকে) বা প্রতিটি পুনরাবৃত্তির পরে নির্ধারণ করা হয়, তবে কম্পাইলারটি লুপের কাউন্টারটিকে অনুকূল করতে দেয় তবে এটি জোর করে আপনি যে ফলাফলটি চান তা বাস্তবে পরিণত করতে যাতে এটি এটি সঞ্চয় করতে পারে।
পিটার কর্ডস

1
@ ডেভিস্লোর: আমি মনে করি জাস্টটি জিএনইউ সি এর মতো ইনলাইন এসএম সিনট্যাক্স পেয়েছে আপনি কমপ্লায়ারকে কোনও রেজিস্টারে সংরক্ষণের জন্য বাধ্য না করে জোর করে ইনলাইন এসএম ব্যবহার করতে পারেন । প্রতিটি লুপ পুনরাবৃত্তির ফলাফল হিসাবে এটি ব্যবহার করা এটিকে অপ্টিমাইজ করা থেকে দূরে রাখতে পারে। (তবে সাবধান না হলে অটো-ভেক্টরাইজিং থেকেও)। উদাহরণস্বরূপ , এমএসভিসিতে "এস্কেপ" এবং "ক্লোবার" সমতুল্য 2 ম্যাক্রো ব্যাখ্যা করেছেন (কীভাবে তাদের এমএসভিসিতে পোর্ট করবেন যা জিজ্ঞাসা করার সময় যা সম্ভব নয়) এবং চ্যানডলার ক্যারথের আলোচনার লিঙ্ক যেখানে তিনি তাদের ব্যবহার দেখান।
পিটার
আমাদের সাইট ব্যবহার করে, আপনি স্বীকার করেছেন যে আপনি আমাদের কুকি নীতি এবং গোপনীয়তা নীতিটি পড়েছেন এবং বুঝতে পেরেছেন ।
Licensed under cc by-sa 3.0 with attribution required.