وظيفة Math.Sin (مزدوجة) لوحدة معالجة الرسومات

مقدمة


كنت بحاجة إلى حساب القوس بدقة أكبر على معالج بطاقة الفيديو في الوقت الحقيقي.

لم يحدد المؤلف هدفًا لتجاوز الوظيفة المعيارية System.Math.Sin () (C #) ولم يصل إليها.

نتيجة العمل وخياري (لمن لا يريد القراءة):

Sin_3 (راد)
using System; class Math_d { const double PI025 = Math.PI / 4; /// <summary> 2^17 = 131072 (1 ),   10000 ( ),  2^21 = 22097152 (16 )   +-1 ( ) (  ) </summary> const int length_mem = 22097152; const int length_mem_M1 = length_mem - 1; /// <summary>    sin,    . </summary> static double[] mem_sin; /// <summary>    cos,    . </summary> static double[] mem_cos; /// <summary>  ,   sin,    . </summary> public static void Initialise() { Ini_Mem_Sin(); Ini_Mem_Cos(); } /// <summary>       Cos,   . </summary> /// <param name="rad"></param> public static double Sin_3(double rad) { double rad_025; int i; //    //if (rad < 0) { rad = -rad + Math.PI; } i = (int)(rad / PI025); //   rad_025 = rad - PI025 * i; //     ( ) i = i & 7; //      8 //    switch (i) { case 0: return Sin_Lerp(rad_025); case 1: return Cos_Lerp(PI025 - rad_025); case 2: return Cos_Lerp(rad_025); case 3: return Sin_Lerp(PI025 - rad_025); case 4: return -Sin_Lerp(rad_025); case 5: return -Cos_Lerp(PI025 - rad_025); case 6: return -Cos_Lerp(rad_025); case 7: return -Sin_Lerp(PI025 - rad_025); } return 0; } /// <summary>   sin    </summary> static void Ini_Mem_Sin() { double rad; mem_sin = new double[length_mem]; for (int i = 0; i < length_mem; i++) { rad = (i * PI025) / length_mem_M1; mem_sin[i] = Math.Sin(rad); } } /// <summary>   cos    </summary> static void Ini_Mem_Cos() { double rad; mem_cos = new double[length_mem]; for (int i = 0; i < length_mem; i++) { rad = (i * PI025) / length_mem_M1; mem_cos[i] = Math.Cos(rad); } } /// <summary>      sin  0  pi/4. </summary> /// <param name="rad">     0  pi/4. </param> static double Sin_Lerp(double rad) { int i_0; int i_1; double i_0d; double percent; double a; double b; double s; percent = rad / PI025; i_0d = percent * length_mem_M1; i_0 = (int)i_0d; i_1 = i_0 + 1; a = mem_sin[i_0]; b = mem_sin[i_1]; s = i_0d - i_0; return Lerp(a, b, s); } /// <summary>      cos  0  pi/4. </summary> /// <param name="rad">     0  pi/4. </param> static double Cos_Lerp(double rad) { int i_0; int i_1; double i_0d; double percent; double a; double b; double s; percent = rad / PI025; i_0d = percent * length_mem_M1; i_0 = (int)i_0d; i_1 = i_0 + 1; a = mem_cos[i_0]; b = mem_cos[i_1]; s = i_0d - i_0; return Lerp(a, b, s); } /// <summary>      . (return a + s * (b - a)) </summary> /// <param name="a">  . </param> /// <param name="b">  . </param> /// <param name="s">  . 0 = a, 1 = b, 0.5 =   a  b. </param> public static double Lerp(double a, double b, double s) { return a + s * (b - a); } } 


أسباب النشر


  • لا توجد وظيفة Sin قياسية للمضاعفة في لغة HLSL (ولكن هذا ليس دقيقًا)
  • هناك القليل من المعلومات المتاحة على الإنترنت حول هذا الموضوع.

المناهج المدروسة



المعلمات المحللة


  • الدقة في الرياضيات
  • السرعة فيما يتعلق بالرياضيات

بالإضافة إلى التحليل ، سنقوم بتحسين أدائهم.

رتب تايلور


الإيجابيات:

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

السلبيات:

  • سرعة منخفضة جدًا (4-10٪)
    يستغرق الأمر الكثير من التكرارات للحصول على الدقة أقرب إلى دقة Math.Sin ، مما يؤدي إلى أبطأ 25 مرة من الوظيفة القياسية.
  • كلما كانت الزاوية أكبر ، انخفضت الدقة
    كلما زادت الزاوية التي تم إدخالها في الوظيفة ، زادت الحاجة إلى التكرار لتحقيق نفس الدقة مثل Math.Sin.

الشكل الأصلي (السرعة: 4٪):

تتضمن الوظيفة القياسية حساب العوامل ، بالإضافة إلى رفع كل تكرار إلى قوة.

الصورة

معدل (السرعة: 10٪):

يمكن تقليل حساب بعض الدرجات في دورة (a * = aa؛) ، ويمكن حساب العوامل الأخرى مسبقًا ووضعها في مصفوفة ، بينما لا يمكن رفع العلامات (+ ، - ، + ، ...) إلى قوة ويمكن أيضًا تقليل حسابها باستخدام القيم السابقة.

والنتيجة هي الرمز التالي:

الخطيئة (راد ، خطوات)
  // <summary>  ,    Fact </summary> static double[] fact; /// <summary>            . ///   rad,   . ///  ( Math): 4% (fps)  steps = 17 </summary> /// <param name="rad">   .      pi/4. </param> /// <param name="steps">  :  ,   .  pi/4   E-15  8. </param> public static double Sin(double rad, int steps) { double ret; double a; //,     double aa; // *  int i_f; //  int sign; // (  -  +,     = +) ret = 0; sign = -1; aa = rad * rad; a = rad; i_f = 1; //      for (int n = 0; n < steps; n++) { sign *= -1; ret += sign * a / Fact(i_f); a *= aa; i_f += 2; } return ret; } /// <summary>   (n!).  n > fact.Length,  -1. </summary> /// <param name="n"> ,     . </param> public static double Fact(int n) { if (n >= 0 && n < fact.Length) { return fact[n]; } else { Debug.Log("    . n = " + n + ",   = " + fact.Length); return -1; } } /// <summary>  . </summary> static void Init_Fact() { int steps; steps = 46; fact = new double[steps]; fact[0] = 1; for (int n = 1; n < steps; n++) { fact[n] = fact[n - 1] * n; } } 


منظر فائق (السرعة: 19٪):

نحن نعلم أنه كلما كانت الزاوية أصغر ، كلما قلت الحاجة إلى التكرار. أصغر زاوية نحتاجها هي = 0.25 * PI ، أي 45 درجة. بالنظر إلى Sin و Cos في منطقة 45 درجة ، يمكننا الحصول على جميع القيم من -1 إلى 1 لـ Sin (في منطقة 2 * PI). للقيام بذلك ، نقسم الدائرة (2 * PI) إلى 8 أجزاء ونشير لكل جزء إلى طريقتنا الخاصة لحساب الجيب. علاوة على ذلك ، لتسريع الحساب ، سنرفض استخدام وظيفة الحصول على الباقي (٪) (للحصول على موضع الزاوية داخل منطقة 45 درجة):

Sin_2 (راد)
  // <summary>  ,    Fact </summary> static double[] fact; /// <summary>   Sin </summary> /// <param name="rad"></param> public static double Sin_2(double rad) { double rad_025; int i; //rad = rad % PI2; //% -   .  , fps   90  150 (  100 000   ) //rad_025 = rad % PI025; i = (int)(rad / PI025); rad_025 = rad - PI025 * i; i = i & 7; //     8  //    switch (i) { case 0: return Sin(rad_025, 8); case 1: return Cos(PI025 - rad_025, 8); case 2: return Cos(rad_025, 8); case 3: return Sin(PI025 - rad_025, 8); case 4: return -Sin(rad_025, 8); case 5: return -Cos(PI025 - rad_025, 8); case 6: return -Cos(rad_025, 8); case 7: return -Sin(PI025 - rad_025, 8); } return 0; } /// <summary>            . ///   rad,   . ///  ( Math): 10% (fps)  steps = 17 </summary> /// <param name="rad">   .      pi/4. </param> /// <param name="steps">  :  ,   .  pi/4   E-15  8. </param> public static double Sin(double rad, int steps) { double ret; double a; //,     double aa; // *  int i_f; //  int sign; // (  -  +,     = +) ret = 0; sign = -1; aa = rad * rad; a = rad; i_f = 1; //      for (int n = 0; n < steps; n++) { sign *= -1; ret += sign * a / Fact(i_f); a *= aa; i_f += 2; } return ret; } /// <summary>            . ///   rad,   . ///  ( Math): 10% (fps), 26% (test)  steps = 17 </summary> /// <param name="rad">   .      pi/4. </param> /// <param name="steps">  :  ,   .  pi/4   E-15  8. </param> public static double Cos(double rad, int steps) { double ret; double a; double aa; // *  int i_f; //  int sign; // (  -  +,     = +) ret = 0; sign = -1; aa = rad * rad; a = 1; i_f = 0; //      for (int n = 0; n < steps; n++) { sign *= -1; ret += sign * a / Fact(i_f); a *= aa; i_f += 2; } return ret; } /// <summary>   (n!).  n > fact.Length,  -1. </summary> /// <param name="n"> ,     . </param> public static double Fact(int n) { if (n >= 0 && n < fact.Length) { return fact[n]; } else { Debug.Log("    . n = " + n + ",   = " + fact.Length); return -1; } } /// <summary>  . </summary> static void Init_Fact() { int steps; steps = 46; fact = new double[steps]; fact[0] = 1; for (int n = 1; n < steps; n++) { fact[n] = fact[n - 1] * n; } } 


كثيرات الحدود


لقد جئت عبر هذه الطريقة على الإنترنت ، وكان المؤلف بحاجة إلى وظيفة بحث سريع عن Sin لمضاعفة الدقة المنخفضة (الخطأ <0.000 001) دون استخدام مكتبات القيم المحسوبة مسبقًا.

الإيجابيات:

  • سرعة عالية (9-84٪)
    في البداية ، أظهر كثير الحدود الذي تم طرحه بدون تغييرات سرعة 9 ٪ من Math.Sin الأصلي ، وهو أبطأ 10 مرات. بفضل التغييرات الصغيرة ، ترتفع السرعة بشكل حاد إلى 84٪ ، وهذا ليس سيئًا إذا أغلقت عينيك للدقة.
  • لا حسابات أولية إضافية والذاكرة المطلوبة
    إذا كان أعلاه وأدناه نحتاج إلى تكوين صفائف من المتغيرات من أجل تسريع الحسابات ، ثم هنا تم حساب جميع المعاملات الرئيسية بلطف ووضعها في الصيغة بواسطة المؤلف نفسه في شكل ثوابت.
  • دقة أعلى من Mathf.Sin (تعويم)
    للمقارنة:

    0.84147 1 - Mathf.Sin (1) (محرك الوحدة) ؛
    0.841470984807897 - الرياضيات (1) (وظيفة C # القياسية) ؛
    0.8414709 56802368 - sin (1) (GPU ، لغة hlsl) ؛
    0.84147 1184637935 - Sin_0 (1) .

السلبيات:

  • ليس عالميًا
    لا يمكنك ضبط الدقة يدويًا لأنه من غير المعروف ما هي الأدوات التي استخدمها المؤلف لحساب هذا كثير الحدود.
  • لماذا؟
    لماذا يحتاج المؤلف إلى وظيفة لا تتطلب أي صفائف والتي لها دقة منخفضة (مقارنة بالضعف)؟

العرض الأصلي:

Sin_1 (x)
 /// <summary>  ( Math): 9% (fps)</summary> /// <param name="x">     -2*Pi  2*Pi </param> public static double Sin_1(double x) { return 0.9999997192673006 * x - 0.1666657564532464 * Math.Pow(x, 3) + 0.008332803647181511 * Math.Pow(x, 5) - 0.00019830197237204295 * Math.Pow(x, 7) + 2.7444305061093514e-6 * Math.Pow(x, 9) - 2.442176561869478e-8 * Math.Pow(x, 11) + 1.407555708887347e-10 * Math.Pow(x, 13) - 4.240664814288337e-13 * Math.Pow(x, 15); } 


عرض متفوق:

Sin_0 (راد)
 /// <summary>  ( Math): 83% (fps)</summary> /// <param name="rad">     -2*Pi  2*Pi </param> public static double Sin_0(double rad) { double x; double xx; double ret; xx = rad * rad; x = rad; //1 ret = 0.9999997192673006 * x; x *= xx; //3 ret -= 0.1666657564532464 * x; x *= xx; //5 ret += 0.008332803647181511 * x; x *= xx; //7 ret -= 0.00019830197237204295 * x; x *= xx; //9 ret += 2.7444305061093514e-6 * x; x *= xx; //11 ret -= 2.442176561869478e-8 * x; x *= xx; //13 ret += 1.407555708887347e-10 * x; x *= xx; //15 ret -= 4.240664814288337e-13 * x; return ret; } 


الاستيفاء الخطي


تعتمد هذه الطريقة على الاستكمال الخطي بين نتائج سجلين في مصفوفة.
يتم تقسيم الإدخالات إلى mem_sin و mem_cos ، وتحتوي على النتائج المحسوبة مسبقًا للوظيفة القياسية Math.Sin و Math.Cos على جزء من معلمات الإدخال من 0 إلى 0.25 * PI.

لا يختلف مبدأ التلاعب بزاوية من 0 إلى 45 درجة عن النسخة المحسنة من سلسلة Taylor ، ولكن في نفس الوقت يتم استدعاء دالة تكتشف - بين سجلين هناك زاوية - وتجد قيمة بينهما.

الإيجابيات:

  • سرعة عالية (65٪)
    نظرًا لبساطة خوارزمية الاستيفاء ، تصل السرعة إلى 65 ٪ من سرعة Math.Sin. أعتقد أن السرعة> 33٪ مرضية.
  • دقة عالية
    مثال لحالة رفض نادرة:
    0.255835595715180 - الرياضيات.
    0.2558355957151 79 - Sin_3 .
  • قدم سريعة
    أحب هذه الوظيفة لأنها ولدت في عذاب كتبتها وتجاوزت المتطلبات: السرعة> 33٪ ، الدقة فوق 1e-14. سأعطيها اسمًا فخورًا - Vēlōx Pes.

السلبيات:

  • يتطلب مكانًا في الذاكرة
    للعمل ، يجب عليك أولاً حساب مصفوفتين: من أجل الخطيئة و cos ؛ كل صفيف يزن ~ 16 ميجابايت (16 * 2 = 32 ميجابايت)

العرض الأصلي:

Sin_3 (راد)
 class Math_d { const double PI025 = Math.PI / 4; /// <summary> 2^17 = 131072 (1 ),   10000 ( ),  2^21 = 22097152 (16 )   +-1 ( ) (  ) </summary> const int length_mem = 22097152; const int length_mem_M1 = length_mem - 1; /// <summary>    sin,    . </summary> static double[] mem_sin; /// <summary>    cos,    . </summary> static double[] mem_cos; /// <summary>  ,   sin,    . </summary> public static void Initialise() { Ini_Mem_Sin(); Ini_Mem_Cos(); } /// <summary>       Cos,   . </summary> /// <param name="rad"></param> public static double Sin_3(double rad) { double rad_025; int i; //    //if (rad < 0) { rad = -rad + Math.PI; } i = (int)(rad / PI025); //   rad_025 = rad - PI025 * i; //     ( ) i = i & 7; //      8 //    switch (i) { case 0: return Sin_Lerp(rad_025); case 1: return Cos_Lerp(PI025 - rad_025); case 2: return Cos_Lerp(rad_025); case 3: return Sin_Lerp(PI025 - rad_025); case 4: return -Sin_Lerp(rad_025); case 5: return -Cos_Lerp(PI025 - rad_025); case 6: return -Cos_Lerp(rad_025); case 7: return -Sin_Lerp(PI025 - rad_025); } return 0; } /// <summary>   sin    </summary> static void Ini_Mem_Sin() { double rad; mem_sin = new double[length_mem]; for (int i = 0; i < length_mem; i++) { rad = (i * PI025) / length_mem_M1; mem_sin[i] = Math.Sin(rad); } } /// <summary>   cos    </summary> static void Ini_Mem_Cos() { double rad; mem_cos = new double[length_mem]; for (int i = 0; i < length_mem; i++) { rad = (i * PI025) / length_mem_M1; mem_cos[i] = Math.Cos(rad); } } /// <summary>      sin  0  pi/4. </summary> /// <param name="rad">     0  pi/4. </param> static double Sin_Lerp(double rad) { int i_0; int i_1; double i_0d; double percent; double a; double b; double s; percent = rad / PI025; i_0d = percent * length_mem_M1; i_0 = (int)i_0d; i_1 = i_0 + 1; a = mem_sin[i_0]; b = mem_sin[i_1]; s = i_0d - i_0; return Lerp(a, b, s); } /// <summary>      cos  0  pi/4. </summary> /// <param name="rad">     0  pi/4. </param> static double Cos_Lerp(double rad) { int i_0; int i_1; double i_0d; double percent; double a; double b; double s; percent = rad / PI025; i_0d = percent * length_mem_M1; i_0 = (int)i_0d; i_1 = i_0 + 1; a = mem_cos[i_0]; b = mem_cos[i_1]; s = i_0d - i_0; return Lerp(a, b, s); } /// <summary>      . (return a + s * (b - a)) </summary> /// <param name="a">  . </param> /// <param name="b">  . </param> /// <param name="s">  . 0 = a, 1 = b, 0.5 =   a  b. </param> public static double Lerp(double a, double b, double s) { return a + s * (b - a); } } 



UPD: خطأ ثابت في تحديد الفهرس في Sin_Lerp () و Cos_Lerp () و Ini_Mem_Sin () و Ini_Mem_Cos ().

Source: https://habr.com/ru/post/ar426355/


All Articles