এই পয়েন্টারটি ব্যবহার করে হট লুপে অদ্ভুত ডিওপটিমাইজেশন হয়


122

আমি সম্প্রতি একটি অদ্ভুত ডিওপিমাইজেশন পেয়েছি (বা বরং অপটিমাইজেশনের সুযোগ মিস)।

8-বিট পূর্ণসংখ্যার 3-বিট পূর্ণসংখ্যার অ্যারেগুলিকে দক্ষভাবে আনপ্যাক করার জন্য এই ফাংশনটি বিবেচনা করুন। এটি প্রতিটি লুপের পুনরাবৃত্তিতে 16 টি ints প্যাক করে:

void unpack3bit(uint8_t* target, char* source, int size) {
   while(size > 0){
      uint64_t t = *reinterpret_cast<uint64_t*>(source);
      target[0] = t & 0x7;
      target[1] = (t >> 3) & 0x7;
      target[2] = (t >> 6) & 0x7;
      target[3] = (t >> 9) & 0x7;
      target[4] = (t >> 12) & 0x7;
      target[5] = (t >> 15) & 0x7;
      target[6] = (t >> 18) & 0x7;
      target[7] = (t >> 21) & 0x7;
      target[8] = (t >> 24) & 0x7;
      target[9] = (t >> 27) & 0x7;
      target[10] = (t >> 30) & 0x7;
      target[11] = (t >> 33) & 0x7;
      target[12] = (t >> 36) & 0x7;
      target[13] = (t >> 39) & 0x7;
      target[14] = (t >> 42) & 0x7;
      target[15] = (t >> 45) & 0x7;
      source+=6;
      size-=6;
      target+=16;
   }
}

কোডের অংশগুলির জন্য উত্পন্ন সমাবেশ এখানে রয়েছে:

 ...
 367:   48 89 c1                mov    rcx,rax
 36a:   48 c1 e9 09             shr    rcx,0x9
 36e:   83 e1 07                and    ecx,0x7
 371:   48 89 4f 18             mov    QWORD PTR [rdi+0x18],rcx
 375:   48 89 c1                mov    rcx,rax
 378:   48 c1 e9 0c             shr    rcx,0xc
 37c:   83 e1 07                and    ecx,0x7
 37f:   48 89 4f 20             mov    QWORD PTR [rdi+0x20],rcx
 383:   48 89 c1                mov    rcx,rax
 386:   48 c1 e9 0f             shr    rcx,0xf
 38a:   83 e1 07                and    ecx,0x7
 38d:   48 89 4f 28             mov    QWORD PTR [rdi+0x28],rcx
 391:   48 89 c1                mov    rcx,rax
 394:   48 c1 e9 12             shr    rcx,0x12
 398:   83 e1 07                and    ecx,0x7
 39b:   48 89 4f 30             mov    QWORD PTR [rdi+0x30],rcx
 ...

এটি বেশ কার্যকর দেখায়। কেবলমাত্র একটি shift rightএকটি দ্বারা অনুসরণ and, এবং তারপর একটি storeকরতে targetবাফার। তবে এখন দেখুন, আমি যখন কাঠামোর কোনও পদ্ধতিতে ফাংশনটি পরিবর্তন করি তখন কী হয়:

struct T{
   uint8_t* target;
   char* source;
   void unpack3bit( int size);
};

void T::unpack3bit(int size) {
        while(size > 0){
           uint64_t t = *reinterpret_cast<uint64_t*>(source);
           target[0] = t & 0x7;
           target[1] = (t >> 3) & 0x7;
           target[2] = (t >> 6) & 0x7;
           target[3] = (t >> 9) & 0x7;
           target[4] = (t >> 12) & 0x7;
           target[5] = (t >> 15) & 0x7;
           target[6] = (t >> 18) & 0x7;
           target[7] = (t >> 21) & 0x7;
           target[8] = (t >> 24) & 0x7;
           target[9] = (t >> 27) & 0x7;
           target[10] = (t >> 30) & 0x7;
           target[11] = (t >> 33) & 0x7;
           target[12] = (t >> 36) & 0x7;
           target[13] = (t >> 39) & 0x7;
           target[14] = (t >> 42) & 0x7;
           target[15] = (t >> 45) & 0x7;
           source+=6;
           size-=6;
           target+=16;
        }
}

আমি ভেবেছিলাম উত্পন্ন সমাবেশটি একই রকম হওয়া উচিত, তবে তা হয় না। এখানে এটির একটি অংশ:

...
 2b3:   48 c1 e9 15             shr    rcx,0x15
 2b7:   83 e1 07                and    ecx,0x7
 2ba:   88 4a 07                mov    BYTE PTR [rdx+0x7],cl
 2bd:   48 89 c1                mov    rcx,rax
 2c0:   48 8b 17                mov    rdx,QWORD PTR [rdi] // Load, BAD!
 2c3:   48 c1 e9 18             shr    rcx,0x18
 2c7:   83 e1 07                and    ecx,0x7
 2ca:   88 4a 08                mov    BYTE PTR [rdx+0x8],cl
 2cd:   48 89 c1                mov    rcx,rax
 2d0:   48 8b 17                mov    rdx,QWORD PTR [rdi] // Load, BAD!
 2d3:   48 c1 e9 1b             shr    rcx,0x1b
 2d7:   83 e1 07                and    ecx,0x7
 2da:   88 4a 09                mov    BYTE PTR [rdx+0x9],cl
 2dd:   48 89 c1                mov    rcx,rax
 2e0:   48 8b 17                mov    rdx,QWORD PTR [rdi] // Load, BAD!
 2e3:   48 c1 e9 1e             shr    rcx,0x1e
 2e7:   83 e1 07                and    ecx,0x7
 2ea:   88 4a 0a                mov    BYTE PTR [rdx+0xa],cl
 2ed:   48 89 c1                mov    rcx,rax
 2f0:   48 8b 17                mov    rdx,QWORD PTR [rdi] // Load, BAD!
 ...

যেমন আপনি দেখতে পাচ্ছেন, আমরা loadপ্রতিটি শিফট ( mov rdx,QWORD PTR [rdi]) এর আগে মেমরি থেকে অতিরিক্ত অতিরিক্ত রিডানড্যান্ট প্রবর্তন করেছি । দেখে মনে হচ্ছে targetপয়েন্টারটি (যা এখন স্থানীয় ভেরিয়েবলের পরিবর্তে সদস্য) এতে স্টোর করার আগে সর্বদা পুনরায় লোড করতে হবে। এটি কোডটি যথেষ্ট গতি কমিয়ে দেয় (আমার পরিমাপের প্রায় 15%)।

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

আমি সদস্য পয়েন্টারটিকে স্থানীয় ভেরিয়েবলে ক্যাশে দেওয়ার চেষ্টা করেছি:

void T::unpack3bit(int size) {
    while(size > 0){
       uint64_t t = *reinterpret_cast<uint64_t*>(source);
       uint8_t* target = this->target; // << ptr cached in local variable
       target[0] = t & 0x7;
       target[1] = (t >> 3) & 0x7;
       target[2] = (t >> 6) & 0x7;
       target[3] = (t >> 9) & 0x7;
       target[4] = (t >> 12) & 0x7;
       target[5] = (t >> 15) & 0x7;
       target[6] = (t >> 18) & 0x7;
       target[7] = (t >> 21) & 0x7;
       target[8] = (t >> 24) & 0x7;
       target[9] = (t >> 27) & 0x7;
       target[10] = (t >> 30) & 0x7;
       target[11] = (t >> 33) & 0x7;
       target[12] = (t >> 36) & 0x7;
       target[13] = (t >> 39) & 0x7;
       target[14] = (t >> 42) & 0x7;
       target[15] = (t >> 45) & 0x7;
       source+=6;
       size-=6;
       this->target+=16;
    }
}

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

  • সুতরাং, সংকলক কেন এই লোডগুলি অপ্টিমাইজ করতে অক্ষম?
  • এটি কি সি ++ মেমরি মডেল যা এটিকে নিষিদ্ধ করে? বা এটি কেবল আমার সংকলকের একটি ঘাটতি?
  • আমার অনুমানটি কি সঠিক বা সঠিক কারণটি অপ্টিমাইজেশন সম্পাদন করা যায় না?

ব্যবহৃত সংকলকটি অপ্টিমাইজেশনের g++ 4.8.2-19ubuntu1সাথে ছিল -O3। আমি clang++ 3.4-1ubuntu3অনুরূপ ফলাফলের সাথে চেষ্টা করেছিলাম : ঝুঁকি এমনকি স্থানীয় targetপয়েন্টারটির সাহায্যে পদ্ধতিটিকে ভেক্টরাইজ করতে সক্ষম । তবে this->targetপয়েন্টার ব্যবহার করে একই ফলাফল পাওয়া যায়: প্রতিটি স্টোরের আগে পয়েন্টারের অতিরিক্ত লোড load

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


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

1
সংকলকটি অনুকূলিত হয় না বলে মনে হয় কারণ তিনি কিছুটা "বাহ্যিক" কোডের মাধ্যমে সদস্যকে অ্যাক্সেস না করে তা নিশ্চিত করতে পারেন না। সুতরাং যদি সদস্যটির বাইরে বাইরে পরিবর্তন করা যায় তবে প্রতিটি বার অ্যাক্সেস করার পরে এটি পুনরায় লোড করা উচিত। এক ধরণের উদ্বায়ী হিসাবে বিবেচিত বলে মনে হচ্ছে ...
জিন-ব্যাপটিস্ট ইউনিস

ব্যবহার না this->করা কেবল সিনট্যাকটিক চিনি। সমস্যাটি ভেরিয়েবলের (স্থানীয় বনাম সদস্য) প্রকৃতির এবং সংস্থাপকটি এই সত্যটি থেকে যে বিষয়গুলি হ্রাস করে তার সাথে সম্পর্কিত।
জিন-ব্যাপটিস্ট ইউনিস

পয়েন্টার এলিয়াস দিয়ে কিছু করার আছে?
ইয়ভেস দাউস্ট

3
আরও শব্দার্থক পদার্থ হিসাবে, "অকাল অপ্টিমাইজেশন" কেবলমাত্র অপটিমাইজেশনের ক্ষেত্রে প্রযোজ্য যা অকাল, অর্থাৎ প্রোফাইলিংয়ের আগে এটি একটি সমস্যা হিসাবে দেখা গেছে। এক্ষেত্রে আপনি অধ্যবসায়ের সাথে প্রোফাইলিং এবং ডিসকোপাইল এবং কোনও সমস্যার উত্স খুঁজে পেয়েছেন এবং একটি সমাধান তৈরি এবং প্রোফাইল করেছেন। সমাধানটি প্রয়োগ করা একেবারে "অকাল" নয়।
raptortech97

উত্তর:


107

পয়েন্টার আলিয়াসিং সমস্যাটি বলে মনে হচ্ছে, বিদ্রূপের মধ্যে thisএবং এর মধ্যে this->target। সংকলকটি আপনি যে আরম্ভ করেছেন তার পরিবর্তে অশ্লীল সম্ভাবনাটি বিবেচনা করছে:

this->target = &this

this->target[0]সেক্ষেত্রে লিখতে লেখার বিষয়বস্তুগুলি this(এবং এইভাবে, this->target) পরিবর্তিত হবে ।

মেমরি এলিয়াসিং সমস্যা উপরের দিকে সীমাবদ্ধ নয়। নীতিগতভাবে, this->target[XX]প্রদত্ত একটি (ইন) এর উপযুক্ত মানটির কোনও ব্যবহারই XXসম্ভবত নির্দেশ করে this

আমি সি সম্পর্কে আরও ভাল পারদর্শী, যেখানে __restrict__কীওয়ার্ডের সাহায্যে পয়েন্টার ভেরিয়েবলগুলি ঘোষণা করে এর প্রতিকার করা যেতে পারে ।


18
আমি এটা নিশ্চিত করতে পারি! targetথেকে পরিবর্তন uint8_tকরা uint16_t(যাতে কঠোর আলিয়াজিংয়ের নিয়ম শুরু হয়) এটি পরিবর্তন করে। সহ uint16_t, বোঝা সর্বদা অপ্টিমাইজ করা হয়।
জক্সিকাইড

1
প্রাসঙ্গিক: stackoverflow.com/questions/16138237/...
user541686

3
বিষয়বস্তু পরিবর্তন করা আপনার thisযা বোঝায় তা নয় (এটি কোনও পরিবর্তনশীল নয়); আপনি এর বিষয়বস্তু পরিবর্তন মানে *this
মার্ক ভ্যান লিউউইন

@ জিক্সাইডাইড মাইন্ড কীভাবে কঠোর ওরফে লাথি মেরে সমস্যা সমাধান করে তা বিশদভাবে বর্ণনা করছেন?
এইচসিএসএফ

33

কঠোর অ্যালাইজিং বিধিগুলি char*অন্য কোনও পয়েন্টারকে উরফ করতে দেয় । সুতরাং আপনার কোড পদ্ধতিতে, কোডটির প্রথম অংশটি this->targetসহ উপনাম হতে পারে this,

target[0] = t & 0x7;
target[1] = (t >> 3) & 0x7;
target[2] = (t >> 6) & 0x7;

আসলে

this->target[0] = t & 0x7;
this->target[1] = (t >> 3) & 0x7;
this->target[2] = (t >> 6) & 0x7;

thisআপনি this->targetকন্টেন্ট সংশোধন করার সময় যেমন পরিবর্তন হতে পারে ।

একবার this->targetস্থানীয় ভেরিয়েবলে ক্যাশ করা হয়ে গেলে স্থানীয় ভেরিয়েবলের সাথে আরসটি আর সম্ভব হয় না।


1
সুতরাং, আমরা কি একটি সাধারণ নিয়ম হিসাবে বলতে পারি: যখনই আপনার একটি char*বা void*আপনার কাঠামোতে রয়েছে, এটি লেখার আগে স্থানীয় ভেরিয়েবলে এটি ক্যাশে করার বিষয়ে নিশ্চিত হন?
জেক্সিকাইড

5
বাস্তবে এটি যখন আপনি একটি ব্যবহার char*করেন তবে সদস্য হিসাবে প্রয়োজনীয় নয়।
জারোড 42

24

এখানে সমস্যাটি কঠোরভাবে উদ্রেককারী যা বলে যে আমাদেরকে চরের * মাধ্যমে উলেফ করার অনুমতি দেওয়া হয় এবং এটি আপনার ক্ষেত্রে সংকলক অনুকূলিতকরণকে বাধা দেয়। আমরা ভিন্ন ধরণের একটি পয়েন্টারটির মাধ্যমে উলামের অনুমতি পাচ্ছি না যা পূর্বনির্ধারিত আচরণ হবে, সাধারণত এসওতে আমরা এই সমস্যাটি দেখতে পাই যা ব্যবহারকারীরা বেমানান পয়েন্টার টাইপের মাধ্যমে উলামের চেষ্টা করছেন ।

Uint8_t স্বাক্ষরবিহীন চর হিসাবে প্রয়োগ করা যুক্তিসঙ্গত বলে মনে হয় এবং আমরা যদি কলিরুতে সিএসডিডিন্টটি দেখি তবে এতে স্ট্যান্ডিন্ট h অন্তর্ভুক্ত রয়েছে যা uint8_t কে নিম্নরূপে টাইপ করে:

typedef unsigned char       uint8_t;

আপনি যদি অন্য কোনও নন-চরিত্রে ব্যবহার করেন তবে সংকলকটি অনুকূলিত করতে সক্ষম হবে।

এটি খসড়া সি ++ স্ট্যান্ডার্ড বিভাগে 3.10 আচ্ছাদিত রয়েছে যেগুলি মূল্য এবং মূল্যগুলি বলে:

যদি কোনও প্রোগ্রাম নিম্নলিখিত ধরণের একটি ব্যতীত অন্য কোনও গ্লুভের মাধ্যমে কোনও অবজেক্টের সঞ্চিত মান অ্যাক্সেস করার চেষ্টা করে তবে আচরণটি সংজ্ঞায়িত

এবং নিম্নলিখিত বুলেট অন্তর্ভুক্ত:

  • একটি চর বা স্বাক্ষরবিহীন চর প্রকার।

দ্রষ্টব্য, আমি সম্ভাব্য কাজের চারপাশে একটি প্রশ্ন পোস্ট করেছি এমন একটি প্রশ্নে যা জিজ্ঞাসা করে যে কবে uint8_t ≠ স্বাক্ষরযুক্ত চর? এবং সুপারিশটি ছিল:

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

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


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