รหัสเครื่อง x86 (SIMD 4x float โดยใช้ 128-bit SSE1 & AVX) 94 ไบต์
รหัสเครื่อง x86 (SIMD 4x สองเท่าโดยใช้ AVX 256 บิต) 123 ไบต์
float
ผ่านกรณีทดสอบในคำถาม แต่ด้วยขีด จำกัด การออกจากลูปขนาดเล็กพอที่จะทำให้เกิดขึ้นได้ง่าย ๆ ที่จะติดอยู่ในการวนซ้ำไม่สิ้นสุดด้วยอินพุตแบบสุ่ม
คำสั่ง SSE1 แบบบรรจุเดียวมีความยาว 3 ไบต์ แต่คำสั่ง SSE2 และคำสั่ง AVX แบบง่ายมีความยาว 4 ไบต์ (คำแนะนำแบบสเกลาร์แบบเดียวเช่นsqrtss
นั้นมีความยาว 4 ไบต์ด้วยเหตุนี้ฉันจึงใช้sqrtps
แม้ว่าฉันจะใส่ใจแค่องค์ประกอบที่ต่ำเท่านั้นมันไม่ได้ช้ากว่า sqrts บนฮาร์ดแวร์ที่ทันสมัย) ฉันใช้ AVX สำหรับปลายทางที่ไม่ทำลายเพื่อบันทึก 2 ไบต์เทียบกับ movaps + op
ในรุ่นสองเรายังสามารถทำคู่movlhps
เพื่อคัดลอกชิ้น 64 บิต (เพราะบ่อยครั้งที่เราเพียง แต่ดูแลเกี่ยวกับองค์ประกอบที่ต่ำของผลรวมแนวนอน) ผลรวมของแนวนอน 256 บิต SIMD เวกเตอร์ยังต้องเป็นพิเศษvextractf128
ที่จะได้รับในช่วงครึ่งสูงเมื่อเทียบกับ2x ช้า แต่มีขนาดเล็กhaddps
กลยุทธ์ในการลอย double
รุ่นยังต้องการค่าคงที่ 2x 8 ไบต์แทน 2x 4 ไบต์ โดยรวมแล้วจะออกมาใกล้เคียงกับขนาด 4/3 ของfloat
รุ่น
mean(a,b) = mean(a,a,b,b)
สำหรับทั้ง 4 วิธีนี้เราสามารถทำซ้ำอินพุตได้สูงสุด 4 อิลิเมนต์และไม่ต้องใช้ความยาว = 2 ดังนั้นเราสามารถ hardcode เรขาคณิตหมายถึงเป็น 4-root = sqrt (sqrt) ตัวอย่างเช่น และเราต้องการค่าคงที่หนึ่ง FP เท่านั้น, 4.0
.
เรามีเวกเตอร์ SIMD เดียวทั้ง 4 [a_i, b_i, c_i, d_i]
ตัว จากนั้นเราคำนวณ 4 หมายถึงสเกลาร์ในรีจิสเตอร์แยกกันและสับกลับเข้าด้วยกันสำหรับการทำซ้ำครั้งถัดไป (การใช้งานในแนวนอนบนเวกเตอร์ SIMD นั้นไม่สะดวก แต่เราต้องทำสิ่งเดียวกันสำหรับทั้ง 4 องค์ประกอบในกรณีที่มันเพียงพอฉันเริ่มต้นรุ่น x87 แต่มันเริ่มยาวและไม่สนุกเลย)
เงื่อนไข loop-exit ของ}while(quadratic - harmonic > 4e-5)
(หรือค่าคงที่ที่น้อยกว่าdouble
)ขึ้นอยู่กับคำตอบ R ของ @ RobinRyderและคำตอบ Java ของ Kevin Cruijssen : ค่าสมการกำลังสองเป็นขนาดที่ใหญ่ที่สุดเสมอและค่าเฉลี่ยฮาร์มอนิกจะเล็กที่สุดเสมอ ดังนั้นเราสามารถตรวจสอบเดลต้าระหว่างสองคนนี้เพื่อตรวจจับการลู่เข้า เราคืนค่าเฉลี่ยเลขคณิตเป็นผลลัพธ์สเกลาร์ โดยปกติแล้วระหว่างสองสิ่งเหล่านี้และอาจเป็นข้อผิดพลาดที่ปัดเศษได้น้อยที่สุด
รุ่นลูกลอย : callable เช่นเดียวfloat meanmean_float_avx(__m128);
กับหาเรื่องและส่งกลับค่าใน xmm0 (ดังนั้น x86-64 System V หรือ Windows x64 vectorcall แต่ไม่ใช่ x64 fastcall) หรือประกาศ return-type __m128
เพื่อที่คุณจะได้ค่าเฉลี่ยกำลังสองและฮาร์มอนิกสำหรับการทดสอบ
การปล่อยให้สิ่งนี้แยก 2 ส่วนfloat
ใน xmm0 และ xmm1 จะมีราคาเพิ่ม 1 ไบต์: เราต้องการ a ที่shufps
มี imm8 (แทนที่จะเป็นเพียงunpcklps xmm0,xmm0
) เพื่อสับเปลี่ยนกันและทำซ้ำ 2 อินพุต
40 address align 32
41 code bytes global meanmean_float_avx
42 meanmean_float_avx:
43 00000000 B9[52000000] mov ecx, .arith_mean ; allows 2-byte call reg, and a base for loading constants
44 00000005 C4E2791861FC vbroadcastss xmm4, [rcx-4] ; float 4.0
45
46 ;; mean(a,b) = mean(a,b,a,b) for all 4 types of mean
47 ;; so we only ever have to do the length=4 case
48 0000000B 0F14C0 unpcklps xmm0,xmm0 ; [b,a] => [b,b,a,a]
49
50 ; do{ ... } while(quadratic - harmonic > threshold);
51 .loop:
52 ;;; XMM3 = geometric mean: not based on addition. (Transform to log would be hard. AVX512ER has exp with 23-bit accuracy, but not log. vgetexp = floor(lofg2(x)), so that's no good.)
53 ;; sqrt once *first*, making magnitudes closer to 1.0 to reduce rounding error. Numbers are all positive so this is safe.
54 ;; both sqrts first was better behaved, I think.
55 0000000E 0F51D8 sqrtps xmm3, xmm0 ; xmm3 = 4th root(x)
56 00000011 F30F16EB movshdup xmm5, xmm3 ; bring odd elements down to even
57 00000015 0F59EB mulps xmm5, xmm3
58 00000018 0F12DD movhlps xmm3, xmm5 ; high half -> low
59 0000001B 0F59DD mulps xmm3, xmm5 ; xmm3[0] = hproduct(sqrt(xmm))
60 ; sqrtps xmm3, xmm3 ; sqrt(hprod(sqrt)) = 4th root(hprod)
61 ; common final step done after interleaving with quadratic mean
62
63 ;;; XMM2 = quadratic mean = max of the means
64 0000001E C5F859E8 vmulps xmm5, xmm0,xmm0
65 00000022 FFD1 call rcx ; arith mean of squares
66 00000024 0F14EB unpcklps xmm5, xmm3 ; [quad^2, geo^2, ?, ?]
67 00000027 0F51D5 sqrtps xmm2, xmm5 ; [quad, geo, ?, ?]
68
69 ;;; XMM1 = harmonic mean = min of the means
70 0000002A C5D85EE8 vdivps xmm5, xmm4, xmm0 ; 4/x
71 0000002E FFD1 call rcx ; arithmetic mean (under inversion)
72 00000030 C5D85ECD vdivps xmm1, xmm4, xmm5 ; 4/. (the factor of 4 cancels out)
73
74 ;;; XMM5 = arithmetic mean
75 00000034 0F28E8 movaps xmm5, xmm0
76 00000037 FFD1 call rcx
77
78 00000039 0F14E9 unpcklps xmm5, xmm1 ; [arith, harm, ?,?]
79 0000003C C5D014C2 vunpcklps xmm0, xmm5,xmm2 ; x = [arith, harm, quad, geo]
80
81 00000040 0F5CD1 subps xmm2, xmm1 ; largest - smallest mean: guaranteed non-negative
82 00000043 0F2E51F8 ucomiss xmm2, [rcx-8] ; quad-harm > convergence_threshold
83 00000047 73C5 jae .loop
84
85 ; return with the arithmetic mean in the low element of xmm0 = scalar return value
86 00000049 C3 ret
87
88 ;;; "constant pool" between the main function and the helper, like ARM literal pools
89 0000004A ACC52738 .fpconst_threshold: dd 4e-5 ; 4.3e-5 is the highest we can go and still pass the main test cases
90 0000004E 00008040 .fpconst_4: dd 4.0
91 .arith_mean: ; returns XMM5 = hsum(xmm5)/4.
92 00000052 C5D37CED vhaddps xmm5, xmm5 ; slow but small
93 00000056 C5D37CED vhaddps xmm5, xmm5
94 0000005A 0F5EEC divps xmm5, xmm4 ; divide before/after summing doesn't matter mathematically or numerically; divisor is a power of 2
95 0000005D C3 ret
96 0000005E 5E000000 .size: dd $ - meanmean_float_avx
0x5e = 94 bytes
(รายชื่อ NASM สร้างขึ้นด้วยnasm -felf64 mean-mean.asm -l/dev/stdout | cut -b -34,$((34+6))-
ถอดส่วนรายชื่อออกและกู้คืนซอร์สด้วยcut -b 34- > mean-mean.asm
)
ผลรวมแนวนอน SIMD และหารด้วย 4 (เช่นค่าเฉลี่ยเลขคณิต) ถูกนำมาใช้ในฟังก์ชั่นแยกต่างหากที่เราcall
(พร้อมตัวชี้ฟังก์ชั่นเพื่อตัดจำหน่ายต้นทุนของที่อยู่) ด้วย4/x
ก่อน / หลังหรือx^2
ก่อนและก่อนและหลังเราจะได้ค่าเฉลี่ยฮาร์มอนิกและค่ากำลังสอง (มันเจ็บปวดที่จะเขียนdiv
คำแนะนำเหล่านี้แทนการคูณด้วยตัวแทนที่0.25
แน่นอน)
ค่าเฉลี่ยเรขาคณิตถูกนำมาใช้แยกกันกับ sqrt คูณและถูกล่ามโซ่ หรือมีหนึ่ง sqrt ก่อนเพื่อลดขนาดเลขชี้กำลังและอาจช่วยความแม่นยำเชิงตัวเลข บันทึกไม่พร้อมใช้งานfloor(log2(x))
ผ่าน AVX512 vgetexpps/pd
เท่านั้น Exp นั้นใช้งานได้ผ่าน AVX512ER (Xeon Phi เท่านั้น) แต่มีความแม่นยำเพียง 2 -23 เท่านั้น
การผสมคำแนะนำ AVX แบบ 128- บิตและ SSE รุ่นเก่าไม่ใช่ปัญหาด้านประสิทธิภาพ การผสม AVX 256 บิตกับ SSE รุ่นเก่าสามารถใช้กับ Haswell ได้ แต่ใน Skylake อาจเป็นเพียงการสร้างการอ้างอิงเท็จสำหรับคำแนะนำ SSE ฉันคิดว่าdouble
เวอร์ชันของฉันหลีกเลี่ยงการใช้เครือข่าย dep-loop ที่ไม่จำเป็นและคอขวดใน div / sqrt latency / throughput
รุ่นสอง:
108 global meanmean_double_avx
109 meanmean_double_avx:
110 00000080 B9[E8000000] mov ecx, .arith_mean
111 00000085 C4E27D1961F8 vbroadcastsd ymm4, [rcx-8] ; float 4.0
112
113 ;; mean(a,b) = mean(a,b,a,b) for all 4 types of mean
114 ;; so we only ever have to do the length=4 case
115 0000008B C4E37D18C001 vinsertf128 ymm0, ymm0, xmm0, 1 ; [b,a] => [b,a,b,a]
116
117 .loop:
118 ;;; XMM3 = geometric mean: not based on addition.
119 00000091 C5FD51D8 vsqrtpd ymm3, ymm0 ; sqrt first to get magnitude closer to 1.0 for better(?) numerical precision
120 00000095 C4E37D19DD01 vextractf128 xmm5, ymm3, 1 ; extract high lane
121 0000009B C5D159EB vmulpd xmm5, xmm3
122 0000009F 0F12DD movhlps xmm3, xmm5 ; extract high half
123 000000A2 F20F59DD mulsd xmm3, xmm5 ; xmm3 = hproduct(sqrt(xmm0))
124 ; sqrtsd xmm3, xmm3 ; xmm3 = 4th root = geomean(xmm0) ;deferred until quadratic
125
126 ;;; XMM2 = quadratic mean = max of the means
127 000000A6 C5FD59E8 vmulpd ymm5, ymm0,ymm0
128 000000AA FFD1 call rcx ; arith mean of squares
129 000000AC 0F16EB movlhps xmm5, xmm3 ; [quad^2, geo^2]
130 000000AF 660F51D5 sqrtpd xmm2, xmm5 ; [quad , geo]
131
132 ;;; XMM1 = harmonic mean = min of the means
133 000000B3 C5DD5EE8 vdivpd ymm5, ymm4, ymm0 ; 4/x
134 000000B7 FFD1 call rcx ; arithmetic mean under inversion
135 000000B9 C5DB5ECD vdivsd xmm1, xmm4, xmm5 ; 4/. (the factor of 4 cancels out)
136
137 ;;; XMM5 = arithmetic mean
138 000000BD C5FC28E8 vmovaps ymm5, ymm0
139 000000C1 FFD1 call rcx
140
141 000000C3 0F16E9 movlhps xmm5, xmm1 ; [arith, harm]
142 000000C6 C4E35518C201 vinsertf128 ymm0, ymm5, xmm2, 1 ; x = [arith, harm, quad, geo]
143
144 000000CC C5EB5CD1 vsubsd xmm2, xmm1 ; largest - smallest mean: guaranteed non-negative
145 000000D0 660F2E51F0 ucomisd xmm2, [rcx-16] ; quad - harm > threshold
146 000000D5 77BA ja .loop
147
148 ; vzeroupper ; not needed for correctness, only performance
149 ; return with the arithmetic mean in the low element of xmm0 = scalar return value
150 000000D7 C3 ret
151
152 ; "literal pool" between the function
153 000000D8 95D626E80B2E113E .fpconst_threshold: dq 1e-9
154 000000E0 0000000000001040 .fpconst_4: dq 4.0 ; TODO: golf these zeros? vpbroadcastb and convert?
155 .arith_mean: ; returns YMM5 = hsum(ymm5)/4.
156 000000E8 C4E37D19EF01 vextractf128 xmm7, ymm5, 1
157 000000EE C5D158EF vaddpd xmm5, xmm7
158 000000F2 C5D17CED vhaddpd xmm5, xmm5 ; slow but small
159 000000F6 C5D35EEC vdivsd xmm5, xmm4 ; only low element matters
160 000000FA C3 ret
161 000000FB 7B000000 .size: dd $ - meanmean_double_avx
0x7b = 123 bytes
C สายรัดทดสอบ
#include <immintrin.h>
#include <stdio.h>
#include <math.h>
static const struct ab_avg {
double a,b;
double mean;
} testcases[] = {
{1, 1, 1},
{1, 2, 1.45568889},
{100, 200, 145.568889},
{2.71, 3.14, 2.92103713},
{0.57, 1.78, 1.0848205},
{1.61, 2.41, 1.98965438},
{0.01, 100, 6.7483058},
};
// see asm comments for order of arith, harm, quad, geo
__m128 meanmean_float_avx(__m128); // or float ...
__m256d meanmean_double_avx(__m128d); // or double ...
int main(void) {
int len = sizeof(testcases) / sizeof(testcases[0]);
for(int i=0 ; i<len ; i++) {
const struct ab_avg *p = &testcases[i];
#if 1
__m128 arg = _mm_set_ps(0,0, p->b, p->a);
double res = meanmean_float_avx(arg)[0];
#else
__m128d arg = _mm_loadu_pd(&p->a);
double res = meanmean_double_avx(arg)[0];
#endif
double allowed_diff = (p->b - p->a) / 100000.0;
double delta = fabs(p->mean - res);
if (delta > 1e-3 || delta > allowed_diff) {
printf("%f %f => %.9f but we got %.9f. delta = %g allowed=%g\n",
p->a, p->b, p->mean, res, p->mean - res, allowed_diff);
}
}
while(1) {
double a = drand48(), b = drand48(); // range= [0..1)
if (a>b) {
double tmp=a;
a=b;
b=tmp; // sorted
}
// a *= 0.00000001;
// b *= 123156;
// a += 1<<11; b += (1<<12)+1; // float version gets stuck inflooping on 2048.04, 4097.18 at fpthreshold = 4e-5
// a *= 1<<11 ; b *= 1<<11; // scaling to large magnitude makes sum of squares loses more precision
//a += 1<<11; b+= 1<<11; // adding to large magnitude is hard for everything, catastrophic cancellation
#if 1
printf("testing float %g, %g\n", a, b);
__m128 arg = _mm_set_ps(0,0, b, a);
__m128 res = meanmean_float_avx(arg);
double quad = res[2], harm = res[1]; // same order as double... for now
#else
printf("testing double %g, %g\n", a, b);
__m128d arg = _mm_set_pd(b, a);
__m256d res = meanmean_double_avx(arg);
double quad = res[2], harm = res[1];
#endif
double delta = fabs(quad - harm);
double allowed_diff = (b - a) / 100000.0; // calculated in double even for the float case.
// TODO: use the double res as a reference for float res
// instead of just checking quadratic vs. harmonic mean
if (delta > 1e-3 || delta > allowed_diff) {
printf("%g %g we got q=%g, h=%g, a=%g. delta = %g, allowed=%g\n",
a, b, quad, harm, res[0], quad-harm, allowed_diff);
}
}
}
สร้างด้วย:
nasm -felf64 mean-mean.asm &&
gcc -no-pie -fno-pie -g -O2 -march=native mean-mean.c mean-mean.o
เห็นได้ชัดว่าคุณต้องการซีพียูที่รองรับ AVX หรืออีมูเลเตอร์เช่น Intel SDE เมื่อต้องการคอมไพล์บนโฮสต์โดยไม่รองรับ AVX ดั้งเดิมให้ใช้-march=sandybridge
หรือ-mavx
เรียกใช้: ผ่านกรณีทดสอบแบบกำหนดค่าตายตัว แต่สำหรับรุ่นลอยกรณีทดสอบแบบสุ่มมักจะล้มเหลวใน(b-a)/10000
เกณฑ์ที่กำหนดไว้ในคำถาม
$ ./a.out
(note: empty output before the first "testing float" means clean pass on the constant test cases)
testing float 3.90799e-14, 0.000985395
3.90799e-14 0.000985395 we got q=3.20062e-10, h=3.58723e-05, a=2.50934e-05. delta = -3.5872e-05, allowed=9.85395e-09
testing float 0.041631, 0.176643
testing float 0.0913306, 0.364602
testing float 0.0922976, 0.487217
testing float 0.454433, 0.52675
0.454433 0.52675 we got q=0.48992, h=0.489927, a=0.489925. delta = -6.79493e-06, allowed=7.23169e-07
testing float 0.233178, 0.831292
testing float 0.56806, 0.931731
testing float 0.0508319, 0.556094
testing float 0.0189148, 0.767051
0.0189148 0.767051 we got q=0.210471, h=0.210484, a=0.21048. delta = -1.37389e-05, allowed=7.48136e-06
testing float 0.25236, 0.298197
0.25236 0.298197 we got q=0.274796, h=0.274803, a=0.274801. delta = -6.19888e-06, allowed=4.58374e-07
testing float 0.531557, 0.875981
testing float 0.515431, 0.920261
testing float 0.18842, 0.810429
testing float 0.570614, 0.886314
testing float 0.0767746, 0.815274
testing float 0.118352, 0.984891
0.118352 0.984891 we got q=0.427845, h=0.427872, a=0.427863. delta = -2.66135e-05, allowed=8.66539e-06
testing float 0.784484, 0.893906
0.784484 0.893906 we got q=0.838297, h=0.838304, a=0.838302. delta = -7.09295e-06, allowed=1.09422e-06
ข้อผิดพลาดของ FP นั้นเพียงพอที่ความเสียหายของรูปสี่เหลี่ยมจะน้อยกว่าศูนย์สำหรับอินพุตบางตัว
หรือไม่ใส่a += 1<<11; b += (1<<12)+1;
เครื่องหมายข้อคิดเห็น:
testing float 2048, 4097
testing float 2048.04, 4097.18
^C (stuck in an infinite loop).
double
ไม่มีปัญหาเหล่านี้เกิดขึ้นกับ ใส่ข้อคิดเห็นprintf
ก่อนการทดสอบแต่ละครั้งเพื่อดูว่าเอาต์พุตว่างเปล่า (ไม่มีอะไรจากif(delta too high)
บล็อก)
สิ่งที่ต้องทำ: ใช้double
เวอร์ชันเป็นข้อมูลอ้างอิงสำหรับfloat
เวอร์ชันแทนที่จะดูว่าพวกเขามาบรรจบกันกับรูปสี่เหลี่ยมอันตรายอย่างไร