Seperti yang ditunjukkan pada artikel sebelumnya, bagian 132-145. Dieksekusi untuk x dalam kisaran [0,126, 0,855469]. Baik. Mari kita coba menulis fungsi yang, dalam batasan yang diberikan, akan lebih akurat dan, mungkin, lebih cepat.
Cara kita menggunakannya cukup jelas. Akurasi penghitungan perlu diperluas untuk memasukkan lebih banyak tempat desimal. Solusi yang jelas adalah memilih tipe ganda panjang, menghitung di dalamnya, dan kemudian mengubahnya kembali. Dari segi akurasi, solusinya harus bagus, tapi dari segi performa, mungkin ada masalah. Bagaimanapun, long double adalah jenis data yang agak eksotis dan dukungannya dalam prosesor modern bukanlah prioritas. Pada instruksi SSE / AVX x86_64 dengan tipe data ini tidak berfungsi. Koprosesor matematis akan "terhempas".
Lalu apa yang harus Anda pilih? Mari kita lihat lebih dekat argumen dan batas fungsinya.
Mereka berada di wilayah 1.0. Itu. sebenarnya, kita tidak membutuhkan floating point. Mari gunakan bilangan bulat 64-bit saat menghitung fungsi. Ini akan memberi kita 10-11 bit tambahan ke presisi asli. Mari kita cari tahu cara menggunakan angka-angka ini. Angka dalam format ini direpresentasikan sebagai a / d , di mana a adalah bilangan bulat, dan d adalah pembagi yang kita pilih konstanta untuk semua variabel dan simpan "di memori kita", dan bukan di memori komputer. Di bawah ini adalah beberapa operasi untuk nomor tersebut:
Seperti yang Anda lihat, tidak ada yang rumit tentang itu. Rumus terakhir menunjukkan perkalian dengan bilangan bulat apa pun. Perhatikan juga hal yang cukup jelas bahwa hasil perkalian dua variabel integer unsigned dengan ukuran N lebih sering merupakan jumlah ukuran hingga 2 * N inklusif. Penambahan dapat menyebabkan luapan hingga 1 bit ekstra.
Mari kita coba memilih pembagi d . Jelas, di dunia biner, yang terbaik adalah memilihnya sebagai pangkat dua, agar tidak membagi, tetapi hanya memindahkan register, misalnya. Kekuatan apa dari dua yang harus Anda pilih? Temukan petunjuknya di instruksi mesin perkalian. Misalnya, instruksi standar MUL dalam sistem x86 mengalikan 2 register dan menulis hasilnya ke 2 register juga, di mana 1 register adalah "bagian atas" dari hasil, dan yang kedua adalah bagian bawah.
Misalnya, jika kita memiliki 2 bilangan 64-bit, maka hasilnya adalah bilangan 128-bit yang ditulis menjadi dua register 64-bit. Sebut saja RH - huruf "atas", dan RL - huruf "kecil" 1 . Kemudian secara matematis hasilnya bisa ditulis... Sekarang kita menggunakan rumus di atas dan menulis perkaliannya
Dan ternyata hasil perkalian dua bilangan fixed-point ini adalah registernya ...
Lebih mudah lagi untuk sistem Aarch64. Instruksi "UMULH" mengalikan dua register dan menulis bagian "atas" dari perkalian ke register ke-3.
Baiklah kalau begitu. Kami telah menentukan nomor fixed-point, tetapi masih ada satu masalah. Angka negatif. Dalam deret Taylor, ekspansi dilakukan dengan tanda variabel. Untuk mengatasi masalah ini, kami mengubah rumus untuk menghitung polinomial dengan metode Goner, menjadi bentuk berikut:
Periksa apakah secara matematis sama persis dengan rumus aslinya. Namun di setiap tanda kurung ada sejumlah formulirselalu positif. Itu. konversi ini memungkinkan ekspresi dievaluasi sebagai bilangan bulat tak bertanda.
constexpr mynumber toint = {{0x00000000, 0x43F00000}}; /* 18446744073709551616 = 2^64 */
constexpr mynumber todouble = {{0x00000000, 0x3BF00000}}; /* ~5.42101086242752217003726400434E-20 = 2^-64 */
double sin_e7(double xd) {
uint64_t x = xd * toint.x;
uint64_t xx = mul2(x, x);
uint64_t res = tsx[19];
for(int i = 17; i >= 3; i -= 2) {
res = tsx[i] - mul2(res, xx);
}
res = mul2(res, xx);
res = x - mul2(x, res);
return res * todouble.x;
}
Nilai tsx [i]
constexpr array<uint64_t, 18> tsx = { // 2^64/i!
0x0000000000000000LL,
0x0000000000000000LL,
0x8000000000000000LL,
0x2aaaaaaaaaaaaaaaLL, // Change to 0x2aaaaaaaaaaaaaafLL and check.
0x0aaaaaaaaaaaaaaaLL,
0x0222222222222222LL,
0x005b05b05b05b05bLL,
0x000d00d00d00d00dLL,
0x0001a01a01a01a01LL,
0x00002e3bc74aad8eLL,
0x0000049f93edde27LL,
0x0000006b99159fd5LL,
0x00000008f76c77fcLL,
0x00000000b092309dLL,
0x000000000c9cba54LL,
0x0000000000d73f9fLL,
0x00000000000d73f9LL,
0x000000000000ca96LL
};
Dimana dalam format titik tetap. Kali ini, untuk kenyamanan, saya memposting semua kode di fast_sine github , menyingkirkan quadmath untuk kompatibilitas dengan dentang dan lengan. Dan saya mengubah sedikit metode penghitungan kesalahan.
Perbandingan fungsi sinus standar dan fungsi titik tetap diberikan dalam dua tabel di bawah ini. Tabel pertama menunjukkan keakuratan penghitungan (untuk x86_64 dan ARM) sama sekali. Tabel kedua adalah perbandingan kinerja.
| Fungsi | Jumlah kesalahan | Nilai ULP maksimum | Deviasi rata-rata |
| sin_e7 | 0,0822187% | 0,504787 | 7.10578e-20 |
| sin_e7a | 0,0560688% | 0,503336 | 2.0985e-20 |
| std :: sin | 0,234681% | 0,515376 | --- |
Selama pengujian, nilai sinus "sebenarnya" dihitung menggunakan perpustakaan MPFR... Nilai ULP maksimum dianggap sebagai deviasi maksimum dari nilai "sebenarnya". Persentase kesalahan - jumlah kasus ketika nilai yang dihitung dari fungsi sinus oleh kami atau libm tidak cocok dengan nilai sinus yang dibulatkan menjadi dua kali lipat. Nilai deviasi rata-rata menunjukkan "arah" kesalahan kalkulasi: perkiraan terlalu tinggi atau terlalu rendah dari nilai. Seperti yang Anda lihat dari tabel, fungsi kita cenderung melebih-lebihkan nilai sinus. Ini bisa diperbaiki! Siapa bilang nilai tsx harus persis sama dengan koefisien deret Taylor. Ide yang agak jelas menyarankan dirinya untuk memvariasikan nilai koefisien untuk meningkatkan keakuratan pendekatan dan menghilangkan komponen kesalahan yang konstan. Sangat sulit untuk membuat variasi seperti itu dengan benar. Tapi kita bisa mencoba. Mari kita ambil contohNilai ke-4 dari larik koefisien tsx (tsx [3]) dan ubah angka terakhir a menjadi f. Mari restart program dan lihat akurasinya (sin_e7a). Lihat, ini sedikit, tapi bertambah! Kami menambahkan metode ini ke celengan kami.
Sekarang mari kita lihat performanya. Untuk pengujian, saya mengambil apa yang ada di tangan i5 mobile dan raspberry keempat yang sedikit di-overclock (Raspberry PI 4 8GB), GCC10 dari distribusi Ubuntu 20.04 x64 untuk kedua sistem.
| Fungsi | x86_64 kali, s | Waktu ARM, s |
| sin_e7 | 0.174371 | 0.469210 |
| std :: sin | 0.154805 | 0.447807 |
Saya tidak berpura-pura lebih akurat dalam pengukuran ini. Variasi beberapa puluh persen dimungkinkan tergantung pada beban prosesor. Kesimpulan utama bisa dibuat seperti ini. Beralih ke aritmatika integer tidak memberikan peningkatan kinerja pada prosesor modern 2 . Jumlah transistor yang tak terbayangkan dalam prosesor modern memungkinkan untuk melakukan perhitungan kompleks dengan cepat. Tapi, menurut saya, pada prosesor seperti Intel Atom, serta pada pengontrol yang lemah, pendekatan ini dapat memberikan peningkatan kinerja yang signifikan. Bagaimana menurut anda?
Meskipun pendekatan ini memberikan peningkatan akurasi, peningkatan akurasi ini tampaknya lebih menarik daripada berguna. Dalam hal kinerja, pendekatan ini dapat ditemukan sendiri, misalnya, di IoT. Tetapi untuk komputasi kinerja tinggi, ini bukan lagi arus utama. Dalam dunia sekarang ini SSE / AVX / CUDA lebih suka menggunakan penghitungan fungsi paralel. Dan dalam aritmatika floating point. Tidak ada analog paralel dari fungsi MUL. Fungsi itu sendiri lebih merupakan penghargaan terhadap tradisi.
Di bab berikutnya, saya akan menjelaskan bagaimana Anda dapat menggunakan AVX secara efektif untuk kalkulasi. Sekali lagi, mari kita masuk ke kode libm dan mencoba memperbaikinya.
1 Tidak ada register dengan nama seperti itu di prosesor yang saya kenal. Nama-nama telah dipilih misalnya.
2Perlu dicatat di sini bahwa ARM saya dilengkapi dengan versi terbaru dari coprocessor matematika. Jika prosesor meniru kalkulasi floating point, hasilnya bisa sangat berbeda.