وظيفة الكلمة الصحيحة عدد صحيح في C++




performance x86-64 (4)

الصب إلى كثافة العمليات بطيئة.

ربما كنت تعيش تحت صخرة منذ x86-64 ، أو أخطأت في أن هذا لم يكن صحيحًا لفترة x86. :)

يوجد لدى SSE / SSE2 تعليمات للتحويل من خلال الاقتطاع (بدلاً من وضع التقريب الافتراضي). يدعم ISA هذه العملية بدقة على وجه التحديد لأن التحويل باستخدام دلالات C ليس نادرًا في الأكواد البرمجية الفعلية. يستخدم رمز x86-64 سجلات SSE / SSE2 XMM للرياضيات FP العددية ، وليس x87 ، وبسبب هذا وغيرها من الأشياء التي تجعله أكثر فعالية. حتى الشفرة 32 بت الحديثة تستخدم سجلات XMM للرياضيات العددية.

عند التحويل إلى x87 (بدون fisttp ) ، اعتاد fisttp استخدام تغيير وضع التقريب x87 إلى اقتطاع ، تخزين FP على الذاكرة ، ثم تغيير وضع التقريب مرة أخرى. (ثم ​​أعد تحميل عدد صحيح من الذاكرة ، عادةً من محلي على المكدس ، إذا قمت بإجراء المزيد من الأشياء معها.) كان x87 أمرًا فظيعًا لهذا.

نعم كان ذلك بطيئًا بشكل مروع ، على سبيل المثال في عام 2006 عندما تمت كتابة الرابط في إجابة @ Kirjain ، إذا كنت لا تزال تمتلك وحدة المعالجة المركزية 32 بت أو كنت تستخدم وحدة المعالجة المركزية x86-64 لتشغيل رمز 32 بت.

لا يتم دعم التحويل باستخدام وضع التقريب بخلاف الاقتطاع أو الافتراضي (الأقرب) بشكل مباشر ، وحتى SSE4.1 roundps / roundpd كان أفضل رهان هو حيل الأرقام السحرية كما في رابط 2006 من إجابة @ Kirjain.

بعض الحيل لطيفة هناك ، ولكن فقط للعدد الصحيح -> 32 بت. من غير المحتمل أن تكون قيمة التمديد double إذا كان لديك float .

أو أكثر من ذلك ، ببساطة أضف رقمًا كبيرًا لتحريك التقريب ، ثم قم بطرحه مرة أخرى للعودة إلى النطاق الأصلي. يمكن أن يعمل هذا من أجل float دون التوسع إلى double ، لكنني لست متأكدًا من السهولة التي يمكن بها عمل floor .

على أي حال ، الحل الواضح هنا هو _mm256_floor_ps() و _mm256_cvtps_epi32 ( vroundps و vcvtps2dq ). إصدار غير AVX من هذا يمكن أن تعمل مع SSE4.1.

لست متأكدًا مما إذا كان بوسعنا أن نفعل ما هو أفضل ؛ إذا كان لديك مجموعة كبيرة للمعالجة (ولم vcvtps2dq هذا العمل مع أعمال أخرى) ، يمكنك تعيين وضع التقريب MXCSR إلى "نحو -Inf" (الأرضية) واستخدام vcvtps2dq ببساطة (والذي يستخدم وضع التقريب الحالي ). ثم أعدها. ولكن ربما يكون من الأفضل حظر التحويل مؤقتًا أو القيام بذلك سريعًا أثناء قيامك بإنشاء البيانات ، ويفترض من حسابات FP الأخرى التي تحتاج إلى تعيين وضع التقريب FP إلى الافتراضي الأقرب.

roundps / pd / ss / sd هي 2 uops على وحدات المعالجة المركزية Intel ، ولكن فقط uop (لكل مسار 128 بت) على AMD Ryzen. cvtps2dq هو أيضًا 1 uop. معبأة مزدوجة> تحويل int يشمل أيضا خلط ورق اللعب. Scalar FP-> التحويل (الذي ينسخ إلى سجل عدد صحيح) عادة ما يكلف uop إضافية لذلك.

لذلك ، هناك مجال لإمكانية حيل الرقم السحري في الفوز في بعض الحالات ؛ قد يكون من المفيد التحقق إذا كان _mm256_floor_ps() + cvt جزءًا من عنق الزجاجة الحرج (أو على الأرجح إذا كان لديك ضعف وتريد int32).

@ Cássio Renan int foo = floorf(f) سوف int foo = floorf(f) تلقائيًا إذا تم التحويل إلى gcc -O3 -fno-trapping-math (أو -ffast-math ) ، مع -march= شيء يحتوي على SSE4.1 أو AVX. https://godbolt.org/z/ae_KPv

قد يكون ذلك مفيدًا إذا كنت تستخدم هذا مع شفرة العددية الأخرى التي لم يتم توجيهها يدويًا. خاصة إذا كنت تأمل أن يقوم المترجم بتوجيه كل شيء تلقائيًا.

أرغب في تحديد وظيفة عدد صحيح صحيح ، أي تحويل من تعويم أو مزدوج يؤدي اقتطاع نحو ما لا نهاية.

يمكننا أن نفترض أن القيم على هذا النحو بحيث لا يحدث أي تجاوز صحيح. لدي حتى الآن بعض الخيارات

  • يلقي كثافة العمليات. هذا يتطلب معالجة خاصة للقيم السالبة ، حيث يتم اقتطاع المدخل باتجاه الصفر ؛

    I= int(F); if (I < 0 && I != F) I--;
  • صب نتيجة الكلمة إلى كثافة العمليات ؛

    int(floor(F));
  • الانتقال إلى int بتحول كبير للحصول على الإيجابيات (يمكن أن يؤدي هذا إلى نتائج خاطئة للقيم الكبيرة) ؛

    int(F + double(0x7fffffff)) - 0x7fffffff;

الصب إلى كثافة العمليات بطيئة. كذلك إذا الاختبارات. أنا لم توقيت وظيفة الكلمة ، ولكن رأيت المشاركات التي تدعي أنها بطيئة أيضا.

هل يمكنك التفكير في بدائل أفضل من حيث السرعة والدقة أو النطاق المسموح به؟ لا تحتاج إلى أن تكون محمولة. الأهداف هي x86 / x64 البنى الحديثة.


إذا كنت تفعل هذا على دفعات ، فقد يقوم المترجم تلقائيًا بتكوين آلية له ، إذا كنت تعرف ما تفعله. على سبيل المثال ، إليك تطبيق صغير يعمل تلقائيًا على تحويل العوامات إلى أعداد صحيحة ، في دول مجلس التعاون الخليجي:

#include <cmath>

// Compile with -O3 and -march=native to see autovectorization
__attribute__((optimize("-fno-trapping-math")))
void testFunction(float* input, int* output, int length) {
  // Assume the input and output are aligned on a 32-bit boundary.
  // Of course, you have  to ensure this when calling testFunction, or else
  // you will have problems.
  input = static_cast<float*>(__builtin_assume_aligned(input, 32));
  output = static_cast<int*>(__builtin_assume_aligned(output, 32));

  // Also assume the length is a multiple of 32.
  if (length & 31) __builtin_unreachable();

  // Do the conversion
  for (int i = 0; i < length; ++i) {
    output[i] = floor(input[i]);
  }
}

هذا هو التجميع الذي تم إنشاؤه لـ x86-64 (مع تعليمات AVX512):

testFunction(float*, int*, int):
        test    edx, edx
        jle     .L5
        lea     ecx, [rdx-1]
        xor     eax, eax
.L3:
        # you can see here that the conversion was vectorized
        # to a vrndscaleps (that will round the float appropriately)
        # and a vcvttps2dq (thal will perform the conversion)
        vrndscaleps     ymm0, YMMWORD PTR [rdi+rax], 1
        vcvttps2dq      ymm0, ymm0
        vmovdqa64       YMMWORD PTR [rsi+rax], ymm0
        add     rax, 32
        cmp     rax, rdx
        jne     .L3
        vzeroupper
.L5:
        ret

إذا كان هدفك لا يدعم AVX512 ، فسوف يظل يعمل تلقائيًا باستخدام تعليمات SSE4.1 ، على افتراض أن لديك تلك. هذا هو الإخراج مع -O3 -msse4.1 :

testFunction(float*, int*, int):
        test    edx, edx
        jle     .L1
        shr     edx, 2
        xor     eax, eax
        sal     rdx, 4
.L3:
        roundps xmm0, XMMWORD PTR [rdi+rax], 1
        cvttps2dq       xmm0, xmm0
        movaps  XMMWORD PTR [rsi+rax], xmm0
        add     rax, 16
        cmp     rax, rdx
        jne     .L3
.L1:
        ret

نرى ذلك مباشرة على godbolt


إليك تعديلًا لجواب Cássio Renan الممتاز. يستبدل كل ملحقات المحول البرمجي الخاصة بـ C ++ القياسي وهو ، من الناحية النظرية ، محمول على أي مترجم مطابق. بالإضافة إلى ذلك ، فإنه يتحقق من أن الوسائط تتم محاذاتها بشكل صحيح بدلاً من افتراض ذلك. أنه يحسن إلى نفس الرمز.

#include <assert.h>
#include <cmath>
#include <stddef.h>
#include <stdint.h>

#define ALIGNMENT alignof(max_align_t)
using std::floor;

// Compiled with: -std=c++17 -Wall -Wextra -Wpedantic -Wconversion -fno-trapping-math -O -march=cannonlake -mprefer-vector-width=512

void testFunction(const float in[], int32_t out[], const ptrdiff_t length)
{
  static_assert(sizeof(float) == sizeof(int32_t), "");
  assert((uintptr_t)(void*)in % ALIGNMENT == 0);
  assert((uintptr_t)(void*)out % ALIGNMENT == 0);
  assert((size_t)length % (ALIGNMENT/sizeof(int32_t)) == 0);

  alignas(ALIGNMENT) const float* const input = in;
  alignas(ALIGNMENT) int32_t* const output = out;

  // Do the conversion
  for (int i = 0; i < length; ++i) {
    output[i] = static_cast<int32_t>(floor(input[i]));
  }
}

هذا لا يحسن إلى حد كبير على دول مجلس التعاون الخليجي مثل الأصلي ، والتي تستخدم ملحقات غير المحمولة. يدعم المعيار C ++ alignas والإشارات إلى المصفوفات std::align ووظيفة std::align التي تُرجع نطاقًا محاذيًا داخل مخزن مؤقت. ومع ذلك ، لا تجعل أي من هذه المترجمات التي قمت باختبارها إنشاء محاذاة بدلاً من تحميلات ومخازن المتجهات غير المحاذية.

على الرغم من أن alignof(max_align_t) هو 16 فقط على x86_64 ، ومن الممكن تعريف ALIGNMENT كـ 64 الثابت ، هذا لا يساعد أي مترجم على إنشاء رمز أفضل ، لذلك ذهبت لقابلية النقل. سيكون أقرب شيء إلى طريقة محمولة لإجبار المترجم على افتراض أن محاذاة poitner هو استخدام الأنواع من <immintrin.h> ، والتي يقوم معظم المترجمين بدعم x86 ، أو تحديد struct بها محدد alignas . من خلال التحقق من وحدات الماكرو المحددة مسبقًا ، يمكنك أيضًا توسيع ماكرو إلى __attribute__ ((aligned (ALIGNMENT))) على برامج التحويل البرمجي Linux أو __declspec (align (ALIGNMENT)) على برامج التحويل البرمجي لـ Windows وشيء آمن على برنامج التحويل البرمجي الذي لا نعرف عنه ، ولكن يحتاج GCC إلى السمة الموجودة في نوع ما لإنشاء الأحمال والمتاجر المتوافقة فعليًا.

بالإضافة إلى ذلك ، فإن المثال الأصلي يطلق عليه "bulit-in" لإخبار دول مجلس التعاون الخليجي أنه كان من المستحيل أن لا يكون length مضاعفات 32. إذا assert() هذا أو استدعت دالة قياسية مثل abort() ، لا GCC ، Clang أو ICC سيجعل نفس الخصم. لذلك ، فإن معظم الكود الذي يقومون بإنشائه سيتعامل مع الحالة التي يكون فيها length ليس دائريًا لطيفًا من عرض المتجه.

أحد الأسباب المحتملة لذلك هو أن أي تحسين لا يحصل على هذه السرعة الكبيرة: إرشادات الذاكرة غير المحاذاة مع العناوين المحاذاة تكون سريعة على وحدات المعالجة المركزية Intel Intel ، ورمز معالجة الحالة التي يكون فيها length ليس عددًا مستديرًا جيدًا length بضعة بايت ويستمر تشغيله وقت ثابت.

كحاشية سفلية ، يمكن لدول مجلس التعاون الخليجي تحسين الوظائف المضمّنة من <cmath> أفضل من وحدات الماكرو <math.c> في <math.c> .

يحتاج GCC 9.1 إلى مجموعة معينة من الخيارات لإنشاء كود AVX512. بشكل افتراضي ، حتى مع -march=cannonlake ، سيفضل المتجهات 256 بت. يحتاج إلى عرض -mprefer-vector-width=512 لإنشاء رمز 512 بت. (شكرًا لبيتر كوردس على توضيح ذلك). تتبع الحلقة الموجّهة باستخدام شفرة غير مُحوّلة لتحويل أي عناصر متبقية من المصفوفة.

إليك الحلقة الرئيسية المتجهية ، ناقص بعض التهيئة في الوقت الثابت ، والتحقق من الأخطاء ورمز التنظيف الذي سيتم تشغيله مرة واحدة فقط:

.L7:
        vrndscaleps     zmm0, ZMMWORD PTR [rdi+rax], 1
        vcvttps2dq      zmm0, zmm0
        vmovdqu32       ZMMWORD PTR [rsi+rax], zmm0
        add     rax, 64
        cmp     rax, rcx
        jne     .L7

ستلاحظ عين النسر اختلافات اثنين من التعليمات البرمجية التي تم إنشاؤها بواسطة برنامج Cássio Renan: يستخدم٪ zmm بدلاً من سجلات٪ ymm ، ويقوم بتخزين النتائج مع vmovdqu32 غير محاذاة بدلاً من vmovdqa64 محاذاة.

Clang 8.0.0 مع نفس العلامات يجعل خيارات مختلفة حول إلغاء التمرير الحلقات. تعمل كل عملية تكرار على ثمانية موجهات 512 بت (أي ، 128 تعويمًا أحادي الدقة) ، ولكن لا يتم التحكم في التعليمات البرمجية لالتقاط بقايا الطعام. إذا كان هناك ما لا يقل عن 64 تطفو بعد ذلك ، فإنه يستخدم أربع تعليمات AVX512 أخرى لتلك ، ومن ثم ينظف أي إضافات مع حلقة unvectorized.

إذا قمت بترجمة البرنامج الأصلي في Clang ++ ، فسوف يقبله دون تقديم شكوى ، لكنه لن يقوم بنفس التحسينات: لن يظل يفترض أن length هو مضاعف عرض المتجه ، ولا تتم محاذاة المؤشرات.

تفضل رمز AVX512 على AVX256 ، حتى بدون عرض -mprefer-vector-width=512 .

        test    rdx, rdx
        jle     .LBB0_14
        cmp     rdx, 63
        ja      .LBB0_6
        xor     eax, eax
        jmp     .LBB0_13
.LBB0_6:
        mov     rax, rdx
        and     rax, -64
        lea     r9, [rax - 64]
        mov     r10, r9
        shr     r10, 6
        add     r10, 1
        mov     r8d, r10d
        and     r8d, 1
        test    r9, r9
        je      .LBB0_7
        mov     ecx, 1
        sub     rcx, r10
        lea     r9, [r8 + rcx]
        add     r9, -1
        xor     ecx, ecx
.LBB0_9:                                # =>This Inner Loop Header: Depth=1
        vrndscaleps     zmm0, zmmword ptr [rdi + 4*rcx], 9
        vrndscaleps     zmm1, zmmword ptr [rdi + 4*rcx + 64], 9
        vrndscaleps     zmm2, zmmword ptr [rdi + 4*rcx + 128], 9
        vrndscaleps     zmm3, zmmword ptr [rdi + 4*rcx + 192], 9
        vcvttps2dq      zmm0, zmm0
        vcvttps2dq      zmm1, zmm1
        vcvttps2dq      zmm2, zmm2
        vmovups zmmword ptr [rsi + 4*rcx], zmm0
        vmovups zmmword ptr [rsi + 4*rcx + 64], zmm1
        vmovups zmmword ptr [rsi + 4*rcx + 128], zmm2
        vcvttps2dq      zmm0, zmm3
        vmovups zmmword ptr [rsi + 4*rcx + 192], zmm0
        vrndscaleps     zmm0, zmmword ptr [rdi + 4*rcx + 256], 9
        vrndscaleps     zmm1, zmmword ptr [rdi + 4*rcx + 320], 9
        vrndscaleps     zmm2, zmmword ptr [rdi + 4*rcx + 384], 9
        vrndscaleps     zmm3, zmmword ptr [rdi + 4*rcx + 448], 9
        vcvttps2dq      zmm0, zmm0
        vcvttps2dq      zmm1, zmm1
        vcvttps2dq      zmm2, zmm2
        vcvttps2dq      zmm3, zmm3
        vmovups zmmword ptr [rsi + 4*rcx + 256], zmm0
        vmovups zmmword ptr [rsi + 4*rcx + 320], zmm1
        vmovups zmmword ptr [rsi + 4*rcx + 384], zmm2
        vmovups zmmword ptr [rsi + 4*rcx + 448], zmm3
        sub     rcx, -128
        add     r9, 2
        jne     .LBB0_9
        test    r8, r8
        je      .LBB0_12
.LBB0_11:
        vrndscaleps     zmm0, zmmword ptr [rdi + 4*rcx], 9
        vrndscaleps     zmm1, zmmword ptr [rdi + 4*rcx + 64], 9
        vrndscaleps     zmm2, zmmword ptr [rdi + 4*rcx + 128], 9
        vrndscaleps     zmm3, zmmword ptr [rdi + 4*rcx + 192], 9
        vcvttps2dq      zmm0, zmm0
        vcvttps2dq      zmm1, zmm1
        vcvttps2dq      zmm2, zmm2
        vcvttps2dq      zmm3, zmm3
        vmovups zmmword ptr [rsi + 4*rcx], zmm0
        vmovups zmmword ptr [rsi + 4*rcx + 64], zmm1
        vmovups zmmword ptr [rsi + 4*rcx + 128], zmm2
        vmovups zmmword ptr [rsi + 4*rcx + 192], zmm3
.LBB0_12:
        cmp     rax, rdx
        je      .LBB0_14
.LBB0_13:                               # =>This Inner Loop Header: Depth=1
        vmovss  xmm0, dword ptr [rdi + 4*rax] # xmm0 = mem[0],zero,zero,zero
        vroundss        xmm0, xmm0, xmm0, 9
        vcvttss2si      ecx, xmm0
        mov     dword ptr [rsi + 4*rax], ecx
        add     rax, 1
        cmp     rdx, rax
        jne     .LBB0_13
.LBB0_14:
        pop     rax
        vzeroupper
        ret
.LBB0_7:
        xor     ecx, ecx
        test    r8, r8
        jne     .LBB0_11
        jmp     .LBB0_12

يُنشئ ICC 19 أيضًا تعليمات AVX512 ، ولكنه يختلف تمامًا عن clang . إنه يقوم بإعدادات أكثر ثوابت سحرية ، لكنه لا يقوم بإلغاء تحديد أي حلقات ، ويعمل بدلاً من ذلك على ناقل 512 بت.

يعمل هذا الرمز أيضًا على المجمعين والمعماريات الأخرى. (على الرغم من أن MSVC يدعم فقط ISA حتى AVX2 ولا يمكن أن يقوم -march=armv8-a+simd الحلقة تلقائيًا.) في ARM مع -march=armv8-a+simd ، على سبيل المثال ، فإنه ينشئ حلقة frintm v0.4s, v0.4s مع frintm v0.4s, v0.4s و fcvtzs v0.4s, v0.4s .

جربه بنفسك .


لماذا لا تستخدم هذا فقط:

#include <cmath>

auto floor_(float const x) noexcept
{
  int const t(x);

  return t - (t > x);
}