arnaucube
arnaucube/poulpyPublic Self-hosted repository
added spqlios as submodule
Jean-Philippe Bossuat jeanphilippe.bossuat@gmail.com
Mon, 27 Jan 2025 14:10:59 +0100
commit

c30f598776afd4eab03ef931ed983f8290fa2e91

parent

250d1a49423b06414b38529705c4a0f72659f0df

A .gitmodules

@@ -0,0 +1,3 @@

+[submodule "base2k/spqlios-arithmetic"] + path = base2k/spqlios-arithmetic + url = https://github.com/tfhe/spqlios-arithmetic
M Cargo.toml → Cargo.toml

@@ -1,2 +1,2 @@

[workspace] -members = ["math", "sampling", "spqlios", "utils"] +members = ["base2k", "rns", "sampling", "utils"]
M math/benches/ntt.rs → rns/benches/ntt.rs

@@ -1,18 +1,19 @@

use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use math::modulus::WordOps; -use math::ring::Ring; use math::poly::Poly; +use math::ring::Ring; fn ntt(c: &mut Criterion) { - - fn runner<'a, const INPLACE: bool, const LAZY:bool>(ring: &'a Ring<u64>) -> Box<dyn FnMut() + 'a > { + fn runner<'a, const INPLACE: bool, const LAZY: bool>( + ring: &'a Ring<u64>, + ) -> Box<dyn FnMut() + 'a> { let mut a: Poly<u64> = ring.new_poly(); for i in 0..a.n() { a.0[i] = i as u64; } - if INPLACE{ + if INPLACE { Box::new(move || ring.ntt_inplace::<LAZY>(&mut a)) - }else{ + } else { let mut b: Poly<u64> = ring.new_poly(); Box::new(move || ring.ntt::<LAZY>(&a, &mut b)) }

@@ -24,33 +25,43 @@ let mut b: criterion::BenchmarkGroup<'_, criterion::measurement::WallTime> =

c.benchmark_group("ntt"); for log_n in 10..17 { - - let ring = Ring::new(1<<log_n, q, 1); + let ring = Ring::new(1 << log_n, q, 1); let runners: [(String, Box<dyn FnMut()>); 4] = [ - (format!("inplace=true/LAZY=true/q={}", q.log2()), { runner::<true, true>(&ring) }), - (format!("inplace=true/LAZY=false/q={}", q.log2()), { runner::<true, false>(&ring) }), - (format!("inplace=false/LAZY=true/q={}", q.log2()), { runner::<false, true>(&ring) }), - (format!("inplace=false/LAZY=false/q={}", q.log2()), { runner::<false, false>(&ring) }), - ]; + (format!("inplace=true/LAZY=true/q={}", q.log2()), { + runner::<true, true>(&ring) + }), + (format!("inplace=true/LAZY=false/q={}", q.log2()), { + runner::<true, false>(&ring) + }), + (format!("inplace=false/LAZY=true/q={}", q.log2()), { + runner::<false, true>(&ring) + }), + (format!("inplace=false/LAZY=false/q={}", q.log2()), { + runner::<false, false>(&ring) + }), + ]; for (name, mut runner) in runners { let id: BenchmarkId = BenchmarkId::new(name, format!("n={}", 1 << log_n)); - b.bench_with_input(id, &(), |b: &mut criterion::Bencher<'_>, _| b.iter(&mut runner)); + b.bench_with_input(id, &(), |b: &mut criterion::Bencher<'_>, _| { + b.iter(&mut runner) + }); } } } fn intt(c: &mut Criterion) { - - fn runner<'a, const INPLACE: bool, const LAZY:bool>(ring: &'a Ring<u64>) -> Box<dyn FnMut() + 'a > { + fn runner<'a, const INPLACE: bool, const LAZY: bool>( + ring: &'a Ring<u64>, + ) -> Box<dyn FnMut() + 'a> { let mut a: Poly<u64> = ring.new_poly(); for i in 0..a.n() { a.0[i] = i as u64; } - if INPLACE{ + if INPLACE { Box::new(move || ring.intt_inplace::<LAZY>(&mut a)) - }else{ + } else { let mut b: Poly<u64> = ring.new_poly(); Box::new(move || ring.intt::<LAZY>(&a, &mut b)) }

@@ -62,26 +73,31 @@ let mut b: criterion::BenchmarkGroup<'_, criterion::measurement::WallTime> =

c.benchmark_group("intt"); for log_n in 10..17 { - - let ring = Ring::new(1<<log_n, q, 1); + let ring = Ring::new(1 << log_n, q, 1); let runners: [(String, Box<dyn FnMut()>); 4] = [ - (format!("inplace=true/LAZY=true/q={}", q.log2()), { runner::<true, true>(&ring) }), - (format!("inplace=true/LAZY=false/q={}", q.log2()), { runner::<true, false>(&ring) }), - (format!("inplace=false/LAZY=true/q={}", q.log2()), { runner::<false, true>(&ring) }), - (format!("inplace=false/LAZY=false/q={}", q.log2()), { runner::<false, false>(&ring) }), - ]; + (format!("inplace=true/LAZY=true/q={}", q.log2()), { + runner::<true, true>(&ring) + }), + (format!("inplace=true/LAZY=false/q={}", q.log2()), { + runner::<true, false>(&ring) + }), + (format!("inplace=false/LAZY=true/q={}", q.log2()), { + runner::<false, true>(&ring) + }), + (format!("inplace=false/LAZY=false/q={}", q.log2()), { + runner::<false, false>(&ring) + }), + ]; for (name, mut runner) in runners { let id: BenchmarkId = BenchmarkId::new(name, format!("n={}", 1 << log_n)); - b.bench_with_input(id, &(), |b: &mut criterion::Bencher<'_>, _| b.iter(&mut runner)); + b.bench_with_input(id, &(), |b: &mut criterion::Bencher<'_>, _| { + b.iter(&mut runner) + }); } } } -criterion_group!( - benches, - ntt, - intt, -); +criterion_group!(benches, ntt, intt,); criterion_main!(benches);
D spqlios/lib/.clang-format

@@ -1,14 +0,0 @@

-# Use the Google style in this project. -BasedOnStyle: Google - -# Some folks prefer to write "int& foo" while others prefer "int &foo". The -# Google Style Guide only asks for consistency within a project, we chose -# "int& foo" for this project: -DerivePointerAlignment: false -PointerAlignment: Left - -# The Google Style Guide only asks for consistency w.r.t. "east const" vs. -# "const west" alignment of cv-qualifiers. In this project we use "east const". -QualifierAlignment: Left - -ColumnLimit: 120
D spqlios/lib/.gitignore

@@ -1,4 +0,0 @@

-cmake-build-* -.idea - -build/
D spqlios/lib/CMakeLists.txt

@@ -1,69 +0,0 @@

-cmake_minimum_required(VERSION 3.8) -project(spqlios) - -# read the current version from the manifest file -file(READ "manifest.yaml" manifest) -string(REGEX MATCH "version: +(([0-9]+)\\.([0-9]+)\\.([0-9]+))" SPQLIOS_VERSION_BLAH ${manifest}) -#message(STATUS "Version: ${SPQLIOS_VERSION_BLAH}") -set(SPQLIOS_VERSION ${CMAKE_MATCH_1}) -set(SPQLIOS_VERSION_MAJOR ${CMAKE_MATCH_2}) -set(SPQLIOS_VERSION_MINOR ${CMAKE_MATCH_3}) -set(SPQLIOS_VERSION_PATCH ${CMAKE_MATCH_4}) -message(STATUS "Compiling spqlios-fft version: ${SPQLIOS_VERSION_MAJOR}.${SPQLIOS_VERSION_MINOR}.${SPQLIOS_VERSION_PATCH}") - -#set(ENABLE_SPQLIOS_F128 ON CACHE BOOL "Enable float128 via libquadmath") -set(WARNING_PARANOID ON CACHE BOOL "Treat all warnings as errors") -set(ENABLE_TESTING ON CACHE BOOL "Compiles unittests and integration tests") -set(DEVMODE_INSTALL OFF CACHE BOOL "Install private headers and testlib (mainly for CI)") - -if (NOT CMAKE_BUILD_TYPE OR CMAKE_BUILD_TYPE STREQUAL "") - set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Build type: Release or Debug" FORCE) -endif() -message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") - -if (WARNING_PARANOID) - add_compile_options(-Wall -Werror -Wno-unused-command-line-argument) -endif() - -message(STATUS "CMAKE_HOST_SYSTEM_NAME: ${CMAKE_HOST_SYSTEM_NAME}") -message(STATUS "CMAKE_SYSTEM_PROCESSOR: ${CMAKE_SYSTEM_PROCESSOR}") -message(STATUS "CMAKE_SYSTEM_NAME: ${CMAKE_SYSTEM_NAME}") - -if (CMAKE_SYSTEM_PROCESSOR MATCHES "(x86)|(X86)|(amd64)|(AMD64)") - set(X86 ON) - set(AARCH64 OFF) -else () - set(X86 OFF) - # set(ENABLE_SPQLIOS_F128 OFF) # float128 are only supported for x86 targets -endif () -if (CMAKE_SYSTEM_PROCESSOR MATCHES "(aarch64)|(arm64)") - set(AARCH64 ON) -endif () - -if (CMAKE_SYSTEM_NAME MATCHES "(Windows)|(MSYS)") - set(WIN32 ON) -endif () -if (WIN32) - #overrides for win32 - set(X86 OFF) - set(AARCH64 OFF) - set(X86_WIN32 ON) -else() - set(X86_WIN32 OFF) - set(WIN32 OFF) -endif (WIN32) - -message(STATUS "--> WIN32: ${WIN32}") -message(STATUS "--> X86_WIN32: ${X86_WIN32}") -message(STATUS "--> X86_LINUX: ${X86}") -message(STATUS "--> AARCH64: ${AARCH64}") - -# compiles the main library in spqlios -add_subdirectory(spqlios) - -# compiles and activates unittests and itests -if (${ENABLE_TESTING}) - enable_testing() - add_subdirectory(test) -endif() -
D spqlios/lib/CONTRIBUTING.md

@@ -1,77 +0,0 @@

-# Contributing to SPQlios-fft - -The spqlios-fft team encourages contributions. -We encourage users to fix bugs, improve the documentation, write tests and to enhance the code, or ask for new features. -We encourage researchers to contribute with implementations of their FFT or NTT algorithms. -In the following we are trying to give some guidance on how to contribute effectively. - -## Communication ## - -Communication in the spqlios-fft project happens mainly on [GitHub](https://github.com/tfhe/spqlios-fft/issues). - -All communications are public, so please make sure to maintain professional behaviour in -all published comments. See [Code of Conduct](https://www.contributor-covenant.org/version/2/1/code_of_conduct/) for -guidelines. - -## Reporting Bugs or Requesting features ## - -Bug should be filed at [https://github.com/tfhe/spqlios-fft/issues](https://github.com/tfhe/spqlios-fft/issues). - -Features can also be requested there, in this case, please ensure that the features you request are self-contained, -easy to define, and generic enough to be used in different use-cases. Please provide an example of use-cases if -possible. - -## Setting up topic branches and generating pull requests - -This section applies to people that already have write access to the repository. Specific instructions for pull-requests -from public forks will be given later. - -To implement some changes, please follow these steps: - -- Create a "topic branch". Usually, the branch name should be `username/small-title` - or better `username/issuenumber-small-title` where `issuenumber` is the number of - the github issue number that is tackled. -- Push any needed commits to your branch. Make sure it compiles in `CMAKE_BUILD_TYPE=Debug` and `=Release`, with `-DWARNING_PARANOID=ON`. -- When the branch is nearly ready for review, please open a pull request, and add the label `check-on-arm` -- Do as many commits as necessary until all CI checks pass and all PR comments have been resolved. - - > _During the process, you may optionnally use `git rebase -i` to clean up your commit history. If you elect to do so, - please at the very least make sure that nobody else is working or has forked from your branch: the conflicts it would generate - and the human hours to fix them are not worth it. `Git merge` remains the preferred option._ - -- Finally, when all reviews are positive and all CI checks pass, you may merge your branch via the github webpage. - -### Keep your pull requests limited to a single issue - -Pull requests should be as small/atomic as possible. - -### Coding Conventions - -* Please make sure that your code is formatted according to the `.clang-format` file and - that all files end with a newline character. -* Please make sure that all the functions declared in the public api have relevant doxygen comments. - Preferably, functions in the private apis should also contain a brief doxygen description. - -### Versions and History - -* **Stable API** The project uses semantic versioning on the functions that are listed as `stable` in the documentation. A version has - the form `x.y.z` - * a patch release that increments `z` does not modify the stable API. - * a minor release that increments `y` adds a new feature to the stable API. - * In the unlikely case where we need to change or remove a feature, we will trigger a major release that - increments `x`. - - > _If any, we will mark those features as deprecated at least six months before the major release._ - -* **Experimental API** Features that are not part of the stable section in the documentation are experimental features: you may test them at - your own risk, - but keep in mind that semantic versioning does not apply to them. - -> _If you have a use-case that uses an experimental feature, we encourage -> you to tell us about it, so that this feature reaches to the stable section faster!_ - -* **Version history** The current version is reported in `manifest.yaml`, any change of version comes up with a tag on the main branch, and the history between releases is summarized in `Changelog.md`. It is the main source of truth for anyone who wishes to - get insight about - the history of the repository (not the commit graph). - -> Note: _The commit graph of git is for git's internal use only. Its main purpose is to reduce potential merge conflicts to a minimum, even in scenario where multiple features are developped in parallel: it may therefore be non-linear. If, as humans, we like to see a linear history, please read `Changelog.md` instead!_
D spqlios/lib/Changelog.md

@@ -1,18 +0,0 @@

-# Changelog - -All notable changes to this project will be documented in this file. -this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [2.0.0] - 2024-08-21 - -- Initial release of the `vec_znx` (except convolution products), `vec_rnx` and `zn` apis. -- Hardware acceleration available: AVX2 (most parts) -- APIs are documented in the wiki and are in "beta mode": during the 2.x -> 3.x transition, functions whose API is satisfactory in test projects will pass in "stable mode". - -## [1.0.0] - 2023-07-18 - -- Initial release of the double precision fft on the reim and cplx backends -- Coeffs-space conversions cplx <-> znx32 and tnx32 -- FFT-space conversions cplx <-> reim4 layouts -- FFT-space multiplications on the cplx, reim and reim4 layouts. -- In this first release, the only platform supported is linux x86_64 (generic C code, and avx2/fma). It compiles on arm64, but without any acceleration.
D spqlios/lib/LICENSE

@@ -1,201 +0,0 @@

- Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License.
D spqlios/lib/README.md

@@ -1,65 +0,0 @@

-# SPQlios library - - - -The SPQlios library provides fast arithmetic for Fully Homomorphic Encryption, and other lattice constructions that arise in post quantum cryptography. - -<img src="docs/api-full.svg"> - -Namely, it is divided into 4 sections: - -* The low-level DFT section support FFT over 64-bit floats, as well as NTT modulo one fixed 120-bit modulus. It is an upgrade of the original spqlios-fft module embedded in the TFHE library since 2016. The DFT section exposes the traditional DFT, inverse-DFT, and coefficient-wise multiplications in DFT space. -* The VEC_ZNX section exposes fast algebra over vectors of small integer polynomial modulo $X^N+1$. It proposed in particular efficient (prepared) vector-matrix products, scalar-vector products, convolution products, and element-wise products, operations that naturally occurs on gadget-decomposed Ring-LWE coordinates. -* The RNX section is a simpler variant of VEC_ZNX, to represent single polynomials modulo $X^N+1$ (over the reals or over the torus) when the coefficient precision fits on 64-bit doubles. The small vector-matrix API of the RNX section is particularly adapted to reproducing the fastest CGGI-based bootstrappings. -* The ZN section focuses over vector and matrix algebra over scalars (used by scalar LWE, or scalar key-switches, but also on non-ring schemes like Frodo, FrodoPIR, and SimplePIR). - -### A high value target for hardware accelerations - -SPQlios is more than a library, it is also a good target for hardware developers. -On one hand, the arithmetic operations that are defined in the library have a clear standalone mathematical definition. And at the same time, the amount of work in each operations is sufficiently large so that meaningful functions only require a few of these. - -This makes the SPQlios API a high value target for hardware acceleration, that targets FHE. - -### SPQLios is not an FHE library, but a huge enabler - -SPQlios itself is not an FHE library: there is no ciphertext, plaintext or key. It is a mathematical library that exposes efficient algebra over polynomials. Using the functions exposed, it is possible to quickly build efficient FHE libraries, with support for the main schemes based on Ring-LWE: BFV, BGV, CGGI, DM, CKKS. - - -## Dependencies - -The SPQLIOS-FFT library is a C library that can be compiled with a standard C compiler, and depends only on libc and libm. The API -interface can be used in a regular C code, and any other language via classical foreign APIs. - -The unittests and integration tests are in an optional part of the code, and are written in C++. These tests rely on -[```benchmark```](https://github.com/google/benchmark), and [```gtest```](https://github.com/google/googletest) libraries, and therefore require a C++17 compiler. - -Currently, the project has been tested with the gcc,g++ >= 11.3.0 compiler under Linux (x86_64). In the future, we plan to -extend the compatibility to other compilers, platforms and operating systems. - - -## Installation - -The library uses a classical ```cmake``` build mechanism: use ```cmake``` to create a ```build``` folder in the top level directory and run ```make``` from inside it. This assumes that the standard tool ```cmake``` is already installed on the system, and an up-to-date c++ compiler (i.e. g++ >=11.3.0) as well. - -It will compile the shared library in optimized mode, and ```make install``` install it to the desired prefix folder (by default ```/usr/local/lib```). - -If you want to choose additional compile options (i.e. other installation folder, debug mode, tests), you need to run cmake manually and pass the desired options: -``` -mkdir build -cd build -cmake ../src -CMAKE_INSTALL_PREFIX=/usr/ -make -``` -The available options are the following: - -| Variable Name | values | -| -------------------- | ------------------------------------------------------------ | -| CMAKE_INSTALL_PREFIX | */usr/local* installation folder (libs go in lib/ and headers in include/) | -| WARNING_PARANOID | All warnings are shown and treated as errors. Off by default | -| ENABLE_TESTING | Compiles unit tests and integration tests | - ------- - -<img src="docs/logo-sandboxaq-black.svg"> - -<img src="docs/logo-inpher1.png">
D spqlios/lib/docs/api-full.svg

@@ -1,416 +0,0 @@

-<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> - -<svg - width="200.88994mm" - height="172.38969mm" - viewBox="0 0 200.88994 172.38969" - version="1.1" - id="svg1" - xml:space="preserve" - inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)" - sodipodi:docname="api-full.svg" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:xlink="http://www.w3.org/1999/xlink" - xmlns="http://www.w3.org/2000/svg" - xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview - id="namedview1" - pagecolor="#ffffff" - bordercolor="#000000" - borderopacity="0.25" - inkscape:showpageshadow="2" - inkscape:pageopacity="0.0" - inkscape:pagecheckerboard="0" - inkscape:deskcolor="#d1d1d1" - inkscape:document-units="mm" - inkscape:zoom="0.62240079" - inkscape:cx="367.93012" - inkscape:cy="346.23992" - inkscape:window-width="1072" - inkscape:window-height="683" - inkscape:window-x="56" - inkscape:window-y="32" - inkscape:window-maximized="1" - inkscape:current-layer="layer1" /><defs - id="defs1"><marker - style="overflow:visible" - id="RoundedArrow" - refX="0" - refY="0" - orient="auto-start-reverse" - inkscape:stockid="Rounded arrow" - markerWidth="1" - markerHeight="1" - viewBox="0 0 1 1" - inkscape:isstock="true" - inkscape:collect="always" - preserveAspectRatio="xMidYMid"><path - transform="scale(0.7)" - d="m -0.21114562,-4.1055728 6.42229122,3.21114561 a 1,1 90 0 1 0,1.78885438 L -0.21114562,4.1055728 A 1.236068,1.236068 31.717474 0 1 -2,3 v -6 a 1.236068,1.236068 148.28253 0 1 1.78885438,-1.1055728 z" - style="fill:context-stroke;fill-rule:evenodd;stroke:none" - id="path8" /></marker><marker - style="overflow:visible" - id="ConcaveTriangle" - refX="0" - refY="0" - orient="auto-start-reverse" - inkscape:stockid="Concave triangle arrow" - markerWidth="1" - markerHeight="1" - viewBox="0 0 1 1" - inkscape:isstock="true" - inkscape:collect="always" - preserveAspectRatio="xMidYMid"><path - transform="scale(0.7)" - d="M -2,-4 9,0 -2,4 c 2,-2.33 2,-5.66 0,-8 z" - style="fill:context-stroke;fill-rule:evenodd;stroke:none" - id="path7" /></marker></defs><g - inkscape:label="Layer 1" - inkscape:groupmode="layer" - id="layer1" - transform="translate(-4.4292935,-3.5395608)"><rect - style="fill:#ffffff;stroke:none;stroke-width:1.01556;stroke-dasharray:1.42178, 0.710893;stroke-dashoffset:0" - id="rect9" - width="203.19905" - height="174.81824" - x="3.2396183" - y="2.4070516" - rx="3" - ry="3" /><path - style="fill:#000000;stroke-width:0.141927" - d="m 47.127979,35.729714 c -2.191188,-0.177706 -3.753709,-0.609 -5.651502,-1.559917 -2.219299,-1.112026 -4.13594,-2.693687 -5.452762,-4.499784 -0.187893,-0.257693 -0.836686,-1.270382 -1.162921,-1.815182 -0.654751,-1.093375 -1.38865,-3.077478 -1.700287,-4.596741 -0.446686,-2.177613 -0.364498,-4.471764 0.247192,-6.900115 0.381473,-1.514388 0.875683,-2.711363 1.643355,-3.980183 0.639507,-1.056981 1.269028,-1.875068 2.106989,-2.7381173 1.005054,-1.0351484 1.950799,-1.8215221 2.947798,-2.4510868 1.169569,-0.7385264 1.782702,-1.0292324 3.423019,-1.6229531 0.676084,-0.2447109 2.185118,-0.5805031 2.998292,-0.6671812 0.714137,-0.07609 2.942802,-0.065782 3.661279,0.017035 2.874915,0.3311971 5.32212,1.3454049 7.754385,3.2136807 0.786197,0.6038967 1.707201,1.4321514 2.009941,1.8075181 0.108267,0.1342446 0.276237,0.3285756 0.373247,0.4318436 0.281527,0.29966 0.479827,0.569916 0.913574,1.24504 0.883114,1.374577 1.412511,2.458114 1.866371,3.81999 0.557817,1.67381 0.782264,3.102192 0.782544,4.980023 1.4e-4,1.406673 -0.1179,2.303785 -0.501777,3.81149 -0.551077,2.164343 -1.369011,3.763224 -3.035242,5.933148 -0.827677,1.077885 -2.215238,2.326558 -3.535999,3.182061 -0.40393,0.261633 -1.390944,0.806171 -1.888641,1.041963 -1.448751,0.686367 -3.164802,1.163568 -4.785683,1.330809 -0.625037,0.06447 -2.308808,0.07382 -3.013172,0.01669 z m 2.140448,-0.634273 c 1.609061,-0.111052 3.135462,-0.444652 4.543713,-0.993033 0.938984,-0.365644 2.360058,-1.146908 3.262562,-1.793648 1.204984,-0.863523 2.637945,-2.256601 2.806585,-2.728473 0.0613,-0.171706 0.0653,-0.570075 0.008,-0.763113 -0.0244,-0.08142 -0.13361,-0.248083 -0.24269,-0.370326 -0.296347,-0.332132 -0.293497,-0.322941 -0.296137,-0.954822 -0.003,-0.625311 -0.0216,-0.692718 -0.25607,-0.911073 -0.291317,-0.271252 -0.282857,-0.584131 0.0309,-1.143284 0.1193,-0.212572 0.22508,-0.446992 0.23507,-0.520933 0.0222,-0.164368 -0.15427,-0.565646 -0.41554,-0.9449 -0.224947,-0.326524 -0.458227,-0.786603 -0.516007,-1.017644 -0.0438,-0.175016 -0.0369,-0.457598 0.0249,-1.023871 l 0.025,-0.229372 -0.277037,-0.02691 c -0.455987,-0.04429 -0.614227,-0.09192 -0.823037,-0.247717 -0.371157,-0.27693 -0.559684,-0.282931 -1.055534,-0.03363 l -0.300967,0.151315 -0.446177,-0.0079 c -0.25961,-0.0044 -0.466377,0.0087 -0.494487,0.03066 -0.0266,0.02114 -0.19327,0.04971 -0.37044,0.06351 l -0.322127,0.02516 -0.15556,-0.22178 C 54.0694,21.200136 53.73405,20.822909 53.237903,20.314091 l -0.298327,-0.305951 0.0276,-0.625345 0.0276,-0.625347 0.10622,-0.07854 c 0.130907,-0.09679 0.195277,-0.244231 0.179347,-0.41079 -0.0111,-0.115996 -0.0236,-0.127449 -0.15018,-0.137985 -0.132957,-0.01109 -0.141017,-0.02009 -0.217547,-0.244083 l -0.0795,-0.232595 0.16123,-0.140762 c 0.0887,-0.0774 0.423137,-0.32054 0.743264,-0.540276 0.817957,-0.561451 0.751027,-0.476739 0.751027,-0.950647 0,-0.419361 0.0465,-0.564981 0.1805,-0.565182 0.0349,-5.2e-5 0.156527,-0.05591 0.270117,-0.12419 0.318177,-0.191169 0.322367,-0.192069 0.901244,-0.193527 l 0.544497,-0.0017 0.21704,-0.122994 c 0.237087,-0.134359 0.372157,-0.158243 1.334171,-0.235975 0.29553,-0.02385 0.548997,-0.0318 0.563277,-0.01756 0.0142,0.01424 0.0316,0.209698 0.0386,0.434405 0.0106,0.341226 0.023,0.411682 0.0751,0.427607 0.0343,0.01048 0.332797,0.08861 0.663257,0.173627 0.330447,0.085 0.688487,0.196866 0.795634,0.248565 0.28331,0.136674 0.34968,0.103871 0.34968,-0.172841 v -0.217673 l 0.174217,-0.02385 c 0.466187,-0.06386 0.610017,-0.02769 0.982767,0.247638 0.109677,0.08098 0.256997,0.171347 0.327367,0.200753 0.15658,0.06543 0.693627,0.07373 0.693627,0.01066 0,-0.08985 -0.23808,-0.810261 -0.32381,-0.979834 -0.0494,-0.09773 -0.13618,-0.206387 -0.192817,-0.241453 -0.33375,-0.206675 -0.47944,-0.441219 -0.518167,-0.834233 -0.0267,-0.270893 -0.0277,-0.272832 -0.12504,-0.248922 -0.25498,0.06264 -0.36441,0.208781 -0.412947,0.551563 -0.0362,0.255221 -0.0934,0.406248 -0.15415,0.406248 -0.019,0 -0.13566,-0.117568 -0.25933,-0.261267 -0.236267,-0.274509 -0.303807,-0.433111 -0.304157,-0.714156 -1.5e-4,-0.161781 -0.0118,-0.178956 -0.310017,-0.459982 -0.41144,-0.387702 -0.557467,-0.458585 -0.874767,-0.424602 -0.275697,0.02953 -0.271767,0.0159 -0.12193,0.423195 0.0698,0.189736 0.0839,0.202648 0.3984,0.365748 0.179327,0.09299 0.374647,0.208213 0.434047,0.256051 0.10627,0.08561 0.10702,0.08886 0.0469,0.205041 -0.0335,0.06491 -0.0611,0.20181 -0.0611,0.304161 v 0.186094 l -0.17567,0.05984 c -0.288597,0.09833 -0.412327,0.08072 -0.569737,-0.08098 l -0.137047,-0.140816 0.273807,-0.01109 c 0.25116,-0.01013 0.276707,-0.0187 0.308737,-0.102945 0.0536,-0.141103 0.0126,-0.194472 -0.277107,-0.360245 -0.535557,-0.306431 -0.736427,-0.479088 -0.849944,-0.730544 l -0.0563,-0.124784 -0.22531,0.188923 -0.22531,0.188924 -0.269507,-0.02297 c -0.261057,-0.02228 -0.271647,-0.01992 -0.337957,0.07679 -0.0376,0.05486 -0.13748,0.178003 -0.22183,0.273566 -0.20543,0.232744 -0.376867,0.490263 -0.488017,0.733035 -0.0906,0.197922 -0.0938,0.200429 -0.288397,0.223798 -0.34319,0.04123 -0.872637,0.02638 -1.058984,-0.0297 -0.25069,-0.07548 -0.34666,-0.178843 -0.39872,-0.429608 -0.0466,-0.224856 -0.0276,-0.585485 0.0413,-0.780723 0.0824,-0.233557 0.504707,-0.319143 1.097194,-0.222357 0.20115,0.03285 0.379617,0.05975 0.396577,0.05975 0.0314,0 0.13942,-0.36946 0.11482,-0.392567 -0.007,-0.007 -0.0768,-0.03888 -0.15425,-0.07094 -0.159377,-0.06604 -0.164827,-0.09412 -0.0886,-0.456481 l 0.0474,-0.225309 0.370757,-0.06508 c 0.41009,-0.07198 0.40957,-0.07164 0.68023,-0.481919 0.246977,-0.374362 0.489177,-0.465819 1.124821,-0.424725 l 0.39073,0.02525 -0.003,-0.127564 c -0.003,-0.178187 -0.0542,-0.516294 -0.081,-0.543035 -0.0122,-0.01223 -0.14408,0.02167 -0.29298,0.07539 -0.311597,0.112404 -0.348907,0.09963 -0.418237,-0.143156 -0.0603,-0.211211 -0.0917,-0.244275 -0.275967,-0.290672 -0.17435,-0.04394 -0.24335,-0.140124 -0.24335,-0.3393384 0,-0.15508 0.16754,-0.3059507 0.521127,-0.4692787 l 0.2798,-0.129239 -0.27333,-0.2518927 C 57.481609,8.549631 55.822818,7.4780726 54.731104,6.9315699 52.759836,5.9447765 50.713178,5.4911375 48.236063,5.4919405 c -1.313284,4.28e-4 -1.572851,0.03224 -3.285762,0.403137 -0.740967,0.1604174 -2.025878,0.6083704 -2.96657,1.0342121 -0.835908,0.3784057 -1.899004,1.0085554 -1.866584,1.1064154 0.0079,0.02306 0.122338,0.05312 0.254906,0.06692 0.132577,0.0138 0.317089,0.05862 0.41003,0.09969 0.09294,0.04106 0.270369,0.09469 0.394289,0.119202 0.265442,0.0525 0.394297,0.125151 0.394297,0.222294 0,0.03861 0.02962,0.08194 0.06569,0.09627 0.113332,0.0449 0.948174,0.0719 0.948174,0.03066 0,-0.1137234 -0.09508,-0.2834454 -0.176038,-0.3142224 -0.05041,-0.01922 -0.242693,-0.03486 -0.427232,-0.03486 -0.205452,0 -0.335521,-0.01503 -0.335521,-0.0387 0,-0.02132 0.02533,-0.04848 0.05635,-0.06037 0.07111,-0.02726 0.07277,-0.1644987 0.0035,-0.2598777 -0.04595,-0.0629 -0.100018,-0.07286 -0.394219,-0.07286 -0.187534,0 -0.340999,-0.01171 -0.341042,-0.02603 -1.4e-4,-0.05023 0.546416,-0.4549767 0.732186,-0.5422217 0.158051,-0.07426 0.247813,-0.08823 0.567391,-0.08855 l 0.379635,-4.19e-4 0.12731,0.21313 c 0.17146,0.2870547 0.27177,0.3538057 0.567327,0.3775237 0.283587,0.0228 0.472977,-0.04709 0.595957,-0.219771 0.0769,-0.1079497 0.15837,-0.1290907 0.325277,-0.08439 0.12859,0.03442 0.10317,0.1089797 -0.11265,0.3302537 -0.222507,0.228121 -0.365837,0.283918 -0.730054,0.28425 -0.12786,1.14e-4 -0.24279,0.01686 -0.25538,0.0373 -0.0126,0.02036 -0.0114,0.138675 0.003,0.262865 l 0.0255,0.2257894 h 0.462007 c 0.41414,0 0.46427,-0.007 0.483757,-0.06849 0.0552,-0.173968 0.16254,-0.231931 0.42945,-0.231931 h 0.255407 l 0.0435,-0.145174 c 0.0705,-0.235372 0.061,-0.256749 -0.12721,-0.284966 -0.145367,-0.02175 -0.167657,-0.03634 -0.150197,-0.09814 0.0113,-0.03992 0.0287,-0.102158 0.0389,-0.138352 0.0384,-0.1374257 0.158767,-0.1660347 0.702114,-0.1667767 0.28779,-4.28e-4 0.532037,0.0079 0.542757,0.01878 0.0373,0.0373 -0.10074,0.2022107 -0.272667,0.3256847 -0.16907,0.121412 -0.17255,0.128016 -0.15407,0.292007 0.0259,0.229441 0.10916,0.299529 0.393317,0.330909 l 0.2347,0.02595 V 8.1595363 7.7999983 l 0.272247,-0.132314 c 0.33172,-0.1612047 0.820597,-0.2642017 1.123674,-0.2367177 0.16406,0.01485 0.34198,0.07792 0.704937,0.2498907 0.267017,0.126497 0.549517,0.252164 0.627787,0.279254 0.160167,0.05547 0.190787,0.121395 0.196627,0.423474 l 0.004,0.193397 0.18788,0.04508 c 0.10333,0.02481 0.19696,0.05975 0.20807,0.07775 0.0229,0.03695 -0.0753,0.245541 -0.16221,0.344686 -0.0455,0.05189 -0.11839,0.06569 -0.34708,0.06569 H 48.60409 l -0.10112,0.2253094 c -0.0556,0.123919 -0.11284,0.225422 -0.12717,0.225562 -0.136207,8.74e-4 -1.357941,-0.285482 -1.347651,-0.316337 0.007,-0.02184 0.23579,-0.1578954 0.507817,-0.3023704 0.27202,-0.144466 0.494587,-0.282126 0.494587,-0.3059047 0,-0.06674 -0.286427,-0.309899 -0.398017,-0.337907 -0.1329,-0.03337 -0.830777,0.05329 -0.957654,0.118914 -0.0554,0.02865 -0.15747,0.120399 -0.22683,0.203879 -0.10929,0.1315627 -0.15494,0.1564777 -0.342557,0.1870097 -0.11906,0.01939 -0.23312,0.04551 -0.25348,0.05809 -0.066,0.0408 -0.0403,0.201818 0.0391,0.244311 0.10413,0.05574 0.0684,0.09513 -0.13184,0.1455674 -0.0933,0.0235 -0.169617,0.06229 -0.169617,0.08614 0,0.08264 -0.12473,0.180782 -0.22981,0.180782 -0.12179,0 -0.183257,-0.08774 -0.183257,-0.261567 0,-0.1532024 -0.0625,-0.1713654 -0.32657,-0.09492 -0.320737,0.09284 -0.697737,0.2774664 -1.098764,0.5380784 -0.195307,0.1269253 -0.412787,0.2498133 -0.483277,0.2730773 -0.12668,0.04185 -0.12791,0.04429 -0.10469,0.2162656 0.0622,0.460788 0.28245,0.64564 0.870127,0.730292 l 0.335817,0.0484 0.0188,0.41114 c 0.0176,0.385073 0.0239,0.411866 0.0994,0.422629 0.0444,0.0061 0.13848,-0.01267 0.209157,-0.04219 0.10715,-0.04473 0.14216,-0.09113 0.21061,-0.27886 0.10985,-0.301278 0.22201,-0.404318 0.545997,-0.501646 0.481087,-0.144509 0.436837,-0.09081 0.437077,-0.530455 1.2e-4,-0.212144 0.0177,-0.448556 0.039,-0.5253626 0.056,-0.201634 0.20343,-0.3600963 0.362857,-0.3900083 0.1348,-0.02533 0.505777,0.03957 0.905397,0.158287 l 0.199687,0.05932 -0.0266,0.3398719 -0.0266,0.339872 0.17304,-0.01948 c 0.0952,-0.01075 0.325687,-0.09204 0.512247,-0.180694 0.18655,-0.08866 0.347127,-0.153255 0.356827,-0.143558 0.01,0.0097 0.029,0.204124 0.0428,0.432037 l 0.0252,0.414399 0.27636,0.162916 0.276357,0.162925 v 0.231591 c 0,0.269225 -0.0342,0.31432 -0.319187,0.420017 -0.1547,0.05739 -0.18776,0.0857 -0.18776,0.160924 0,0.06028 0.0319,0.10657 0.0939,0.136368 0.577567,0.277525 0.543497,0.250522 0.555947,0.440451 0.0112,0.170954 0.009,0.174474 -0.0999,0.174474 -0.0612,0 -0.218477,-0.03355 -0.349407,-0.0746 -0.13093,-0.04106 -0.26785,-0.07487 -0.30427,-0.07513 -0.0736,-5.68e-4 -0.219147,-0.07391 -0.261777,-0.131922 -0.06,-0.08177 -0.0242,-0.218216 0.084,-0.319187 0.158907,-0.148432 0.148817,-0.278012 -0.0289,-0.371627 -0.0943,-0.04962 -0.15882,-0.02787 -0.15882,0.05373 0,0.07775 -0.18186,0.159056 -0.454997,0.203373 -0.13183,0.0214 -0.25033,0.03905 -0.26332,0.03914 -0.013,1.4e-4 -0.0449,-0.0511 -0.0709,-0.113873 -0.0873,-0.210659 -0.28787,-0.276423 -0.557047,-0.182581 -0.11095,0.0387 -0.11826,0.05259 -0.11826,0.225309 0,0.127326 0.0153,0.184084 0.0495,0.184084 0.0273,0 0.13916,0.02664 0.248717,0.05914 l 0.1992,0.05914 -0.12296,0.126855 -0.12295,0.126864 -0.162887,-0.09546 -0.16289,-0.09546 -0.13022,0.08046 c -0.147277,0.09102 -0.447337,0.410381 -0.555057,0.590752 -0.0512,0.08579 -0.183777,0.179935 -0.463587,0.329354 -0.347817,0.185743 -0.448357,0.265232 -0.894264,0.707037 -0.481227,0.47679 -0.504237,0.506589 -0.545857,0.707114 -0.0505,0.24306 -0.12343,0.323266 -0.509847,0.560395 -0.465997,0.285954 -0.712637,0.506615 -0.845657,0.756551 -0.06543,0.12288 -0.07111,0.184442 -0.04962,0.535104 0.01337,0.217944 0.01241,0.396254 -0.0017,0.396254 -0.01441,0 -0.151438,-0.06875 -0.304457,-0.152687 l -0.278205,-0.152687 -0.01109,-0.250993 -0.01109,-0.250993 -0.769812,-0.02752 c -0.649946,-0.02332 -0.80196,-0.01826 -0.976339,0.03206 -0.244012,0.0705 -0.264367,0.07801 -0.337967,0.124408 -0.03101,0.01957 -0.150032,0.07626 -0.264569,0.126078 -0.294725,0.128148 -0.458497,0.286793 -0.57887,0.560726 -0.05696,0.129642 -0.144379,0.321222 -0.194261,0.425748 -0.144301,0.302342 -0.152714,0.514749 -0.03224,0.814803 0.056,0.139591 0.115017,0.261922 0.131074,0.271846 0.01607,0.01 0.159213,0.03739 0.318137,0.06098 0.377909,0.05617 0.469357,0.02621 0.939945,-0.308153 0.206535,-0.146755 0.436902,-0.298359 0.511935,-0.336902 l 0.136421,-0.07006 0.02315,0.115865 c 0.04455,0.222906 0.0221,0.336342 -0.117647,0.593573 -0.101616,0.187063 -0.129458,0.27071 -0.100009,0.300508 0.02245,0.02271 0.207208,0.07478 0.410598,0.115673 0.203391,0.04088 0.375279,0.07985 0.381989,0.08657 0.007,0.007 0.0332,0.306345 0.05879,0.665873 l 0.04656,0.653686 0.187762,0.04656 c 0.103259,0.0256 0.354208,0.07137 0.557659,0.101827 0.294839,0.04412 0.390931,0.04604 0.473501,0.0098 0.0896,-0.0394 0.564782,-0.54957 0.564782,-0.60645 0,-0.01127 0.0356,-0.0615 0.0792,-0.111663 0.0643,-0.07399 0.10501,-0.08776 0.21592,-0.07329 0.718184,0.09384 0.907174,0.16717 0.994714,0.385962 0.0316,0.07897 0.0852,0.15426 0.119127,0.167277 0.0775,0.0297 0.721997,0.03023 0.832337,5.67e-4 0.0462,-0.01241 0.099,-0.07032 0.12058,-0.132298 l 0.0384,-0.110274 0.198617,0.100237 c 0.23145,0.116809 0.399747,0.300281 0.619127,0.674949 0.10229,0.17471 0.188,0.277375 0.24408,0.292392 0.0475,0.01276 0.266037,0.02341 0.485547,0.02368 l 0.399107,5.67e-4 0.13768,0.199914 c 0.16666,0.241977 0.24679,0.492228 0.24753,0.773011 2.9e-4,0.115472 0.0217,0.25184 0.0477,0.303033 0.0564,0.111549 0.29559,0.254006 0.477517,0.28446 0.0723,0.01214 0.22262,0.0663 0.33407,0.120503 0.189867,0.0923 0.246437,0.09983 0.898274,0.119421 l 0.695647,0.02088 0.104747,0.202937 c 0.14949,0.289588 0.27241,0.401286 0.564447,0.512879 0.14121,0.05399 0.25644,0.117752 0.25609,0.141759 -2.9e-4,0.02402 -0.0327,0.167092 -0.0719,0.317962 -0.0574,0.220862 -0.10327,0.30866 -0.23561,0.450619 -0.254427,0.27292 -0.401577,0.510047 -0.439637,0.70845 -0.0191,0.09974 -0.054,0.434814 -0.0775,0.744618 -0.0629,0.829392 -0.0821,0.89451 -0.314887,1.066425 -0.10614,0.07845 -0.21615,0.170491 -0.24447,0.204614 -0.0425,0.05119 -0.128077,0.06203 -0.490147,0.06203 h -0.438657 l -0.11771,0.12204 c -0.203897,0.211384 -0.246077,0.444975 -0.144557,0.800545 0.0291,0.101853 0.06,0.282303 0.0688,0.400999 0.0132,0.178877 0.004,0.227073 -0.0571,0.281638 -0.17434,0.157125 -0.853417,1.066302 -0.986127,1.320273 -0.14026,0.268386 -0.15393,0.281857 -0.39109,0.385255 -0.135147,0.05897 -0.287987,0.132053 -0.339617,0.162489 -0.26992,0.159108 -0.405447,0.205208 -0.605607,0.205994 -0.23821,8.73e-4 -0.307837,0.0518 -0.288947,0.210938 0.01,0.08098 0.0429,0.112616 0.161387,0.153448 0.0821,0.0283 0.16406,0.06622 0.1821,0.08422 0.054,0.05399 -0.009,0.184477 -0.18451,0.382058 -0.0911,0.102595 -0.165657,0.197039 -0.165657,0.20989 0,0.01284 0.0431,0.07985 0.0958,0.148956 0.088,0.115332 0.1125,0.125588 0.30041,0.125588 h 0.204634 v 0.233635 c 0,0.134865 -0.0342,0.342012 -0.0809,0.489964 l -0.0809,0.25633 0.0883,0.06185 c 0.0486,0.03407 0.190604,0.140229 0.315594,0.236002 0.19877,0.152293 0.22876,0.192016 0.23921,0.3168 0.006,0.07845 -0.005,0.159982 -0.0266,0.181149 -0.0411,0.04106 -0.757404,0.04289 -0.886434,0.0026 -0.0734,-0.02315 -0.262287,-0.177174 -0.680884,-0.555292 -0.11294,-0.102019 -0.28299,-0.313245 -0.37788,-0.469392 -0.0949,-0.156146 -0.254617,-0.416058 -0.354967,-0.577577 -0.16707,-0.268936 -0.18244,-0.313306 -0.18244,-0.526708 0,-0.128165 0.0175,-0.372318 0.0388,-0.542546 0.0496,-0.395382 0.0162,-0.51744 -0.231197,-0.845781 -0.24395,-0.323728 -0.26084,-0.442965 -0.1437,-1.014104 0.0478,-0.233066 0.0759,-0.452409 0.0625,-0.48744 -0.0137,-0.03564 -0.0646,-0.06368 -0.11548,-0.06368 -0.0501,0 -0.10513,-0.02953 -0.12236,-0.06569 -0.0172,-0.03617 -0.0369,-0.623355 -0.0438,-1.304916 l -0.0124,-1.239205 -0.119877,-0.125186 c -0.17938,-0.187307 -0.755857,-0.56119 -0.937127,-0.607779 -0.351822,-0.09042 -0.344642,-0.08491 -0.373472,-0.285298 -0.0444,-0.30824 -0.11389,-0.420584 -0.656709,-1.061777 l -0.521442,-0.615527 0.06482,-0.454689 c 0.03564,-0.250084 0.06587,-0.533863 0.06709,-0.630623 0.0035,-0.274964 0.05102,-0.348486 0.359266,-0.556515 0.37962,-0.256174 0.442694,-0.358681 0.415726,-0.675745 -0.01144,-0.134245 -0.04289,-0.320129 -0.07006,-0.413062 l -0.04927,-0.168988 -0.185892,-0.0044 c -0.102237,-0.0026 -0.276904,-0.02341 -0.388156,-0.04691 -0.111244,-0.02359 -0.281219,-0.04281 -0.377725,-0.04281 -0.199171,0 -0.244511,-0.02927 -0.395145,-0.25509 -0.05914,-0.08867 -0.125963,-0.173155 -0.148485,-0.187762 -0.02254,-0.01459 -0.132105,-0.133154 -0.243505,-0.263468 -0.272562,-0.318826 -0.535505,-0.441706 -1.176986,-0.55005 l -0.140824,-0.02358 v -0.199949 -0.199949 l -0.177619,-0.01992 c -0.133005,-0.01502 -0.214511,-8.73e-4 -0.324515,0.05495 -0.101669,0.05189 -0.182843,0.06753 -0.263624,0.05093 -0.06421,-0.01319 -0.175855,-0.03276 -0.248145,-0.04333 -0.07225,-0.01066 -0.165223,-0.03363 -0.206535,-0.05119 -0.04132,-0.01756 -0.207872,-0.07085 -0.370143,-0.118442 -0.22309,-0.06543 -0.302622,-0.106517 -0.326148,-0.168385 -0.01712,-0.04499 -0.0449,-0.08782 -0.06167,-0.09515 -0.01686,-0.007 -0.06438,-0.08011 -0.105697,-0.161781 -0.04132,-0.08168 -0.116432,-0.19787 -0.166961,-0.258261 l -0.09186,-0.109812 0.06674,-0.173583 c 0.113882,-0.296211 0.114467,-0.331705 0.0079,-0.463967 -0.173749,-0.215027 -0.583998,-0.830573 -0.661259,-0.992162 -0.04202,-0.0879 -0.08904,-0.298542 -0.107688,-0.482539 -0.03704,-0.365311 -0.07976,-0.435252 -0.271662,-0.444407 l -0.12675,-0.0061 0.0061,0.2835 c 0.0087,0.367277 0.100341,0.623965 0.372387,1.038609 0.18812,0.286741 0.218844,0.359223 0.233582,0.551135 l 0.01695,0.220975 -0.210834,-0.01747 c -0.253194,-0.02097 -0.311332,-0.06674 -0.553588,-0.435461 -0.300674,-0.457644 -0.346355,-0.617502 -0.352872,-1.235073 -0.0035,-0.341192 -0.02132,-0.551458 -0.04918,-0.585119 -0.02394,-0.02892 -0.132603,-0.100079 -0.241392,-0.158198 -0.325562,-0.173916 -0.329223,-0.179717 -0.329223,-0.521667 0,-0.268632 -0.0061,-0.296325 -0.05617,-0.246469 -0.138709,0.13871 -0.57417,1.390014 -0.783195,2.250575 -0.09397,0.386856 -0.236107,1.342203 -0.332412,2.234319 -0.02752,0.254636 -0.0484,0.883415 -0.04805,1.445732 5.07e-4,0.848095 0.01354,1.081109 0.08801,1.577173 0.267014,1.777706 0.739717,3.356617 1.397831,4.669054 1.318822,2.630021 3.343825,4.786857 5.978491,6.367706 0.939988,0.564011 2.857197,1.322169 3.819321,1.510359 0.21686,0.04237 0.723797,0.142833 1.126534,0.223151 0.402747,0.08028 0.918151,0.160899 1.145341,0.179052 0.586063,0.04682 1.52304,0.0491 2.159217,0.0052 z" - id="path23" /><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.4, 0.7;stroke-dashoffset:0;marker-end:url(#ConcaveTriangle)" - d="M 50.144569,72.668694 V 52.396365" - id="path24" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.39999998, 0.69999999;marker-end:url(#ConcaveTriangle);stroke-dashoffset:0" - d="M 54.377905,47.986365 V 68.258694" - id="path25" - sodipodi:nodetypes="cc" /><rect - style="fill:#d7f4ee;stroke-width:0.3;stroke-dasharray:0.600001, 0.600001" - id="rect19" - width="137.32346" - height="14.956152" - x="19.258987" - y="103.4627" - rx="5" - ry="5" /><rect - style="fill:#d5f6ff;fill-opacity:1;stroke-width:0.3;stroke-dasharray:0.600001, 0.600001" - id="rect21" - width="75.022408" - height="24.181534" - x="19.258987" - y="77.004356" - rx="5" - ry="5" /><rect - style="fill:#d5f6ff;fill-opacity:1;stroke-width:0.3;stroke-dasharray:0.600001, 0.600001" - id="rect22" - width="29.907101" - height="24.181534" - x="96.517372" - y="77.004356" - rx="5" - ry="5" /><rect - style="fill:#d5f6ff;stroke-width:0.430238;stroke-dasharray:0.860478, 0.860478" - id="rect23" - width="29.096771" - height="24.733751" - x="128.26723" - y="77.004356" - rx="5" - ry="5" /><text - xml:space="preserve" - style="font-size:3.52778px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';fill:#000000;stroke-width:0.3" - x="21.87299" - y="81.671806" - id="text8"><tspan - sodipodi:role="line" - id="tspan8" - style="font-size:3.52778px;fill:#000000;stroke-width:0.3" - x="21.87299" - y="81.671806">VEC_ZNX arithmetic API</tspan></text><text - xml:space="preserve" - style="font-size:3.52778px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';fill:#000000;stroke-width:0.3" - x="23.751926" - y="108.33081" - id="text17"><tspan - sodipodi:role="line" - id="tspan17" - style="font-size:3.52778px;fill:#000000;stroke-width:0.3" - x="23.751926" - y="108.33081">FFT/NTT API</tspan></text><text - xml:space="preserve" - style="font-size:3.52778px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';fill:#000000;stroke-width:0.3" - x="102.77969" - y="81.375206" - id="text18"><tspan - sodipodi:role="line" - id="tspan18" - style="font-size:3.52778px;fill:#000000;stroke-width:0.3" - x="102.77969" - y="81.375206">RNX API</tspan></text><text - xml:space="preserve" - style="font-size:3.52778px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';fill:#000000;stroke-width:0.3" - x="136.22255" - y="81.576088" - id="text19"><tspan - sodipodi:role="line" - id="tspan19" - style="font-size:3.52778px;fill:#000000;stroke-width:0.3" - x="136.22255" - y="81.576088">ZN API</tspan></text><text - xml:space="preserve" - style="font-size:5.64444px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';fill:#d5f6ff;fill-opacity:1;stroke-width:0.3;stroke-dasharray:0.600001, 0.600001" - x="29.593384" - y="21.761526" - id="text24"><tspan - sodipodi:role="line" - id="tspan24" - style="stroke-width:0.3" - x="29.593384" - y="21.761526" /></text><text - xml:space="preserve" - style="font-size:3.52778px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';text-align:center;text-anchor:middle;fill:#666666;stroke-width:0.3" - x="53.504406" - y="40.35569" - id="text25"><tspan - sodipodi:role="line" - id="tspan25" - style="font-size:3.52778px;text-align:center;text-anchor:middle;fill:#666666;stroke-width:0.3" - x="53.504406" - y="40.35569">FHE libraries</tspan><tspan - sodipodi:role="line" - style="font-size:3.52778px;text-align:center;text-anchor:middle;fill:#666666;stroke-width:0.3" - x="53.504406" - y="46.352917" - id="tspan26">BGV, BFV, CKKS</tspan></text><text - xml:space="preserve" - style="font-size:3.52778px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';fill:#666666;stroke-width:0.3" - x="115.64725" - y="40.35569" - id="text31"><tspan - sodipodi:role="line" - style="font-size:3.52778px;fill:#666666;stroke-width:0.3" - x="115.64725" - y="40.35569" - id="tspan31">FHE libraries</tspan><tspan - sodipodi:role="line" - style="font-size:3.52778px;fill:#666666;stroke-width:0.3" - x="115.64725" - y="46.352917" - id="tspan54">TFHE/CGGI, DM</tspan></text><text - xml:space="preserve" - style="font-size:3.52778px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';fill:#666666;stroke-width:0.3" - x="181.2632" - y="40.35569" - id="text35"><tspan - sodipodi:role="line" - style="font-size:3.52778px;fill:#666666;stroke-width:0.3" - x="181.2632" - y="40.35569" - id="tspan35">Other PQC:</tspan><tspan - sodipodi:role="line" - style="font-size:3.52778px;fill:#666666;stroke-width:0.3" - x="181.2632" - y="46.352917" - id="tspan36">NTRU,</tspan><tspan - sodipodi:role="line" - style="font-size:3.52778px;fill:#666666;stroke-width:0.3" - x="181.2632" - y="52.350143" - id="tspan37">Falcon</tspan></text><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.4, 0.7;stroke-dashoffset:0;marker-end:url(#ConcaveTriangle)" - d="M 88.77376,72.668694 V 52.396365" - id="path37" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.39999998, 0.69999999;marker-end:url(#ConcaveTriangle);stroke-dashoffset:0" - d="M 93.007096,47.986365 V 68.258694" - id="path38" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.4, 0.7;stroke-dashoffset:0;marker-end:url(#ConcaveTriangle)" - d="M 118.40721,72.668694 V 52.396365" - id="path39" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.39999998, 0.69999999;marker-end:url(#ConcaveTriangle);stroke-dashoffset:0" - d="M 122.64057,47.986365 V 68.258694" - id="path40" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.39999998, 0.69999999;marker-end:url(#ConcaveTriangle);stroke-dashoffset:0" - d="m 150.15741,72.668694 7.9375,-20.272329" - id="path41" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.39999998, 0.69999999;marker-end:url(#ConcaveTriangle);stroke-dashoffset:0" - d="m 163.38661,47.986365 -7.9375,20.272329" - id="path42" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.39999998, 0.69999999;marker-end:url(#ConcaveTriangle);stroke-dashoffset:0" - d="M 159.77083,98.887499 180.38924,58.727986" - id="path43" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.39999998, 0.69999999;marker-end:url(#ConcaveTriangle);stroke-dashoffset:0" - d="M 182.6127,62.177205 161.99429,102.33673" - id="path44" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.4, 0.7;stroke-dashoffset:0;marker-end:url(#ConcaveTriangle)" - d="M 21.569551,72.668694 V 52.396365" - id="path45" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.39999998, 0.69999999;marker-end:url(#ConcaveTriangle);stroke-dashoffset:0" - d="M 25.802887,47.986365 V 68.258694" - id="path46" - sodipodi:nodetypes="cc" /><text - xml:space="preserve" - style="font-size:3.52778px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';text-align:center;text-anchor:middle;fill:#666666;stroke-width:0.3" - x="24.922491" - y="40.35569" - id="text47"><tspan - sodipodi:role="line" - id="tspan46" - style="font-size:3.52778px;text-align:center;text-anchor:middle;fill:#666666;stroke-width:0.3" - x="24.922491" - y="40.35569">Onion-PIRs</tspan><tspan - sodipodi:role="line" - style="font-size:3.52778px;text-align:center;text-anchor:middle;fill:#666666;stroke-width:0.3" - x="24.922491" - y="46.352917" - id="tspan47">Onion, spiral</tspan></text><image - width="28.970715" - height="28.970715" - preserveAspectRatio="none" - xlink:href="data:image/png;base64,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&#10;yv0Yi4sA0KXlEBAZL/wYtmitr8G4Yx3NhkPoemYTnNATlUrdeaFgllYTNSUbqSvbQUj3NMJSslD7&#10;OuNrcHJWq4X6vcUYd27APzwGXc9++AaFdnkOgKaKfRiKi1CpfdCn5+AfHuuSHC31RozFRbTUVKHr&#10;2Y/gbj3ABZ1is6mJmpKN1O8vITQxg9CUTNSarl9KxGoxU7dnOzWlGwmIjEfXox+awOAuzwHQeGgv&#10;xuIi1Bo/dGk5+IfHuCRHS50B4/ZCWuqN6NJyCI5LcdF3pBHjzo00HtxFSFIvQpN6o9b4HredsabR&#10;qTmsFjPNB3bQVLYNv/A4AhMzUWvdbkX6BuBVJ+z38HLABcDuYz88tgNw2O6//vvcCYEArkVhB0AU&#10;S1sLWz6Ywa5F/6G5uvyoz/zDY0gedzW9r3mgS05quxd/zJYPn6Fhf+lRf64JDCb2tDPJmTQTrT7a&#10;6Tmqt65h3WtTMe5Yh6Wt9e8/V2t80Wfk0W/STPRpuU7P0Vx1gKJXp1K+ZgltTQ3/+0ClIjg+lcxr&#10;HqT76EucnsPSamLT+0+x+8ePMRkrjvrMPzyWlHOuo/eV01D5dPRrJE7pwrlsmzeLhgO7jvpz38AQ&#10;YgedRb/bnkMbFuH0HJUb/2D9G/dj3Lkeq7nt7z9Xa/wI751Pzu2zCEvt6/QcTRVlFL5yDxWFv9DW&#10;/L8GRKVSE5KYTtb1jxM/5Byn5zCbmtj47yfYu2Q+pprKoz4LiIwn9bwbybjsblRq50+3UvLNu2yb&#10;/wKN5XuO+nPfoFDih5xL9q3P4Pf/7d17fBTlvcfxz+4mu9lcSEJISEggFyCkcguIYFW8FS+96KEq&#10;xdZK0VOsBcVqFa2+7EurtaDivZ626PFSPdUj51h77PFQoCpWBQkQIEBCuCSESxIg5LZJNpvsnj8m&#10;1IC57Mw8m8myv/frlVf+mWfzfTLPzPPszDPPJCSHPMfR4nVs+/39NOwrIeD/8r6+PdpFylnTKbxt&#10;OUNyvgaA19tBS0toJv95jx3kwBu/pLH0M/zt3a4w2Oy4R4xl5Jz7SJx0SUj+tgHNwH0D/Uetup5e&#10;DRgaHn/77T2mR9YN+0rYuGwBDftK+twuMW8C59y7gsS8Cab+Xm+8DcfZ8sxiDv3jL31u50pMYcri&#10;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&#10;" - id="image1" - x="12.163744" - y="145.64278" /><image - width="33.19001" - height="33.19001" - preserveAspectRatio="none" - xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABHNCSVQICAgIfAhkiAAAIABJREFU&#10;eJzs3Xd0VNUWBvBvWnrvHRIgoZNC70V6EcHyLCCKKFb02SsWBLFjQ1FEEWk2pENUQOmEDoGEloT0&#10;SnqfeX9gfAikzbl37szk+63Fci2cOXcDJ3P3nHv2PgARERERERERERERERERERERERERERERERER&#10;ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER&#10;ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER&#10;ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER&#10;ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER&#10;ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER&#10;ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER&#10;ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER&#10;EREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREQGASukA&#10;iMg8xIy/38EW5esBwxClYyGyPIY9tTUYtW/T90VKR9JUTACIiDd/IklYVhLABICohePNn0hKlpME&#10;MAEgasF48yeSg2UkAUwAiFoo3vyJ5GT+SQATAKIWiDd/IlMw7ySACQBRC8ObP5EpmW8SwASAqAXh&#10;zZ9ICeaZBDABIGohePMnUpL5JQFMAIhaAN78icyBeSUBTACIrBxv/kTmxHySACYARFaMN38ic2Qe&#10;SQATACIrxZs/kTlTPglgAkBkhXjzJ7IEyiYBTACIrAxv/kSWRLkkgAkAkRXhzZ/IEimTBDABILIS&#10;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&#10;Wbzo+bS2df+WQ4rX2tqWh+Y8nKeffiZTpkzJpZde0mfuJ/lkfy5+Pq2tld2f9fX1+eu/vCVDhw4t&#10;LGapVMq8eY/n9dffKCzmsXIEwHGr69cvN9zw57njjruye/fuSi+nUO+9917mL1iYf1uyJBecf34m&#10;XzgpZzU1VXpZZdHa1paVL72c5S+t1HRWI959b98n+/O8L34xkyZPyjmf+2yll1UWH+/PFStX5v19&#10;ld+ftTrr3xkFAF0y5KOmwLvuvqemmwI/TXv7B/n58hX5+fIVGT1qVCZPnpTx48/L8OHDKr20bnlv&#10;376senV1XvnFL7J1639Wejl0UXv7B1n58itZ+fIrGTV6VCZPnJgJE87L8OHFv7SqJ+3bty+rVq3O&#10;K6/8R5q3bq30cj5Ry7P+nVEAHMWevXtr7ha8ztQPqc/gE4p5tD1q9KhMnXpd5s6dV0i8arW9pSXb&#10;W1ry1FNPZ0zTmEyYMD7njhtbltsFy2HPnj3ZsOG1vPrqqmx6882UOjoqvSQK1LK9JS3bW/LU08+k&#10;acyYjD//vJw7dmxZbhcsh7179mb9xo1Z9erqvLF5c1Xuz3LM+m/c+Hoee/SJQmMeLwXAUdzzj/+U&#10;9J78n1NGNub7t91aWBFw4eRJ2b5te362dFkh8apZR6mULVuas2VLc5588sdpbGzMuHFjc87nfidN&#10;Y5oyZGh1XHp54MCBNG/dmjfe2JQNGzZW/NyUnlEqlbKluTlbmpvzr3kqjY2NGTv28/n8OZ+tqv15&#10;8ODBbGluzqY3Nmf9ho1pbW2t9JI6ddll5Zn1nzVrdsVfm60A6GPebtuVOQ/Ny003T0+/gt5D/42r&#10;r0rLjh3ZvGlzIfFqxa5du7J06bIs/aj4OXXkyDQ1jcno0aNz2um/nVGnn172RsL29vbs2Lkzb721&#10;I9u3bU/z1q1p3dmajl701Iqu2bVrV5Yt25Vly/49STJy5ClpamrKGR/tz9NPO63sjYTt7R9k546d&#10;adnxVlq2t2RLc3NN7c/eMOvfGQVAH7R23bosXLAoV155RSHx+vXvnxnTb+yVTYHHo7Wt7UgX/Usr&#10;P/m14cOHpWFEQ0aM+K2MaBiR4cOHZ0j9iamvH5L6IfU5cfDgDBgwIHV1dZ8UCwfbD6bUUcqHhw6n&#10;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&#10;" - id="image1-3" - x="55.115635" - y="142.72488" /><image - width="30.466211" - height="30.466211" - preserveAspectRatio="none" - xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABHNCSVQICAgIfAhkiAAAHZZJREFU&#10;eJzt3V+MXNd9H/Dfnb2zu6QokSL9RxQ9JBQDih3JDpxarRMrkGsLRRopAh8qUw8xihCIAwFEQwgK&#10;ICQIgUpooaeWMIQ4kZ5SuagEvRTKkw3RqCpbZmWjhQEDVItCrDmWbIkKRWlJmrs7nNuHXVJLcrU7&#10;c+fO3Jk5nw8w2J2799zzu5R2z3fuPTMnC/rWau19LiI7UHcdmyueb7dPPVTlEZ27cx9vzr3KI6Z8&#10;7ilo1F0AADB6AgAAJEgAAIAECQAAkCABAAASJAAAQIIEAABIkAAAAAkSAAAgQQIAACRIAACABAkA&#10;AJAgAQAAEiQAAECC8roLSM1TjzXigXuynvd/8eUiDj3ZHWJFo+PcnXsvnLtzZzRcAQCABAkAAJAg&#10;AQAAEiQAAECCBAAASJAAAAAJEgAAIEECAAAkSAAAgAQJAACQIAEAABIkAABAggQAAEiQAAAACRIA&#10;ACBBAgAAJEgAAIAECQAAkCABAAASJAAAQIIEAABIkAAAAAkSAAAgQQIAACRIAACABAkAAJAgAQAA&#10;EiQAAECCBAAASJAAAAAJEgAAIEECAAAkSAAAgAQJAACQIAEAABKU1V1AlVqtfWcjYm71aXHNj4vN&#10;vs+yq9sUxUe22RoRzTI15nlEo49/9W4R0emU6SkiIpYj4kLp1uublHO/GBHvl269vu0RMV+m4YjP&#10;vRsr/+2r1IySLxice+/7O/cyPa00j+rPfT0X2+2f7xhBPyOR111AxebjwwDQt+LayDAEA/wPXkYz&#10;VgatsTDic5+PkoP1MIz43BsxwO9B1Zz7yDh3+uIWAAAkSAAAgAQJAACQIAEAABIkAABAggQAAEiQ&#10;AAAACRIAACBBAgAAJEgAAIAECQAAkCABAAASJAAAQIKmbTXA0t6874dRNEqtcjuWPnPrXNz5qbFZ&#10;DA9gLBSdpfgf//ZzdZcxFgSAVUWjGcXMFK0mOTMXjXyKzgegAt26CxgjbgEAQIIEAABIkAAAAAkS&#10;AAAgQQIAACRIAACABAkAAJAgAQAAEiQAAECC8lZr73N1F1Gh6fksXwDGTXOaxsw8IjtQdxEAMAEa&#10;0zRmugUAAAkSAAAgQQIAACRIAACABAkAAJAgAQAAEiQAAECCBAAASJAAAAAJEgAAIEECAAAkSAAA&#10;gATldRcwiZoLJ+OWY/vrLmND5yLi1YqP2fra4Wjde7jio9KPW2+K+A9/VK7tWx9EPPIPk98H0+9b&#10;P4h49ed1VzH9BgoATz3W5wWE5p6IZmuQLjd0+PHj0el0h3b8DxUj6AOAcZLnEUcf7XPcm90Xke8e&#10;TkERcehI+Zd6AwWAB+7J+mswvyNifu8gXW7okSeOD+3YAKStkZUY97bsjJgb3rg3SAAwB6AUVwAA&#10;mGwCQBmFAADAZBMAShEAAJhsAkApAgAAk00AKMMtAAAmnABQQuYKAAATTgAowxUAACacAFCKAADA&#10;ZBMAShEAAJhsAkAZxn8AJtxAHwX84st9joTNsxHNU4N0uaHuyAZmCQAgNd2ixLg3eyYiH964N4iB&#10;AsChJ/tdeKe9+ph0AgBAajqdMuPeydXH+LEccAlLN38u2vt/WncZG/rsrXPx+db8ddvbLx2N9rGj&#10;NVREFd76IOKh/6wPYHDmAABAggQAAEiQAAAACRIAACBBAgAAJEgAAIAECQAAkCABAAASJAAAQIIE&#10;AABIkAAAAAkSAAAgQXlE8XzdRVQnezCEGgCGoxtRvFB3EVXJ2+1TD9VdRFVarX37I2Ku7joAmErL&#10;0zRmerUMAAkSAAAgQQIAACRIAACABAkAAJAgAQAAEiQAAECCBAAASJAAAAAJEgAAIEECAAAkSAAA&#10;gAQJAACQoLzuAgAYvl/+6O/jVz/6+7rLGMgXHvl+3SVMFQEAIAGdC+/Fr0+/UXcZjBG3AAAgQQIA&#10;ACRIAACABAkAAJCgvNXa+1zdRVSoWXcBAEyt5jSNmXlEdqDuIgBgMzfMLfa1//KlPJY6M1WW0Jim&#10;MdPbAAGYCJ+4aaGv/d+7sDWWOluHVM3kMwcAABIkAABAggQAAEiQAAAACRIAACBBAgAAJMjbAAHY&#10;0B+ca8QfnOvv9eJv33XXutv/eftHVZREBQQAADZ0YzeLW5ezvtp8uun99+POLQAASJAAAAAJEgAA&#10;IEHmAJTQXDgZtxzbX3cZ62rv/2ndJQAwAQYKAE891ucFhOaeiGZrkC43dPjx49HpdId2/A8VI+gD&#10;gHGS5xFHH+1z3JvdF5HvHk5BEXHoyKul2w4UAB64p79ZoTG/I2J+7yBdbuiRJ44P7dhMl25nKYpL&#10;SxGRRWQREVlkWbb6PLvm+Xo/12fqfZKeRlZi3NuyM2JueONebQEgXa4ATLo3/9vfRPvY0VJtW187&#10;HK17D+sz8T5h0pkEWEYhAAAw2QSAUgQAACabAFCKAADAZBMAynALAIAJJwCUkLkCAMCEEwDKcAUA&#10;gAknAJQiAAAw2QSAUgQAACabAFCG8R+ACTfQJwG++HKfI2HzbETz1CBdbqg7soFZAgDS0W4W8cOt&#10;/f3de+fc20Oqpj7dosS4N3smIh/euDeIgQLAoSf7XXinvfqYdAIAkI7jW7pxfEuff+/f+dlwiqlR&#10;p1Nm3Du5+hg/1gIoYenmzw112d3Wf/3toR0bACLMAQCAJAkAAJAgAQAAEiQAAECCTAJkMhXdWD73&#10;j6WbX1o6P1Db5YXT+ky8z8ua23ZFZF5LMXkEACbS8rl/jB//+7tq6futV56Jt155Rp/6jIiIu/7y&#10;x9G88eMVVsRH+cWZm/va/1KRDamS6SAAADARli/N1F3CVHHdCgASJAAAQIIEAABIkAAAAAkSAAAg&#10;QXlE8XzdRVQnezCEGmCEdm47H3M3bul5/4tLzTi3ODfEita39ZO3x8c+f//I+50y3YjihbqLqEre&#10;bp96qO4iqtJq7dsfEaP/zQKSdcPcYmyZv9hXmzoCwK47/zB23fmHI+93yixP05jp1TIAJEgAAIAE&#10;CQAAkCABAAASJAAAQIIsBkRyGlnEzq1Fn41mIjILkUyrMwvL0e3zfwmYdAIAPfvg5PH4xfe/tbL2&#10;edaILMtWv648v+VL34hGPrvpcdovHR24lkHWf9+5tYj/eXihz0afidj2qdJ9Mt5+5y/+V7z7wXKp&#10;tie++2zk81t73n+pk8eFxet/T/Z85eFo5MN7e+D7bxyPD944PrTjj0Lr3sN1lzBVBAB69v4bx+P9&#10;Df6AfOKLD24eAIoi2scGDwAwLk587zuVHGf33QeHGgA+eOP4xP/uCQDVMgeAEXOdFWAcCACMVFEI&#10;AADjQABgxAQAgHEgADBaxn+AsSAAMGISAMA4EAAYKXMAAMaDtwEyYgIAUM5tH3+3r/3fu7A1zp7v&#10;/TMaUuMKAKPlCgDAWHAFgBGb3ACw989eK9321N/906T6AMafKwCMlDkAAONBAGDEBACAcZC3Wnuf&#10;q7uICjXrLoBNGP+BydWcpjEzj8gO1F0EKZEAgInVmKYx0yTAxOy4/Z7It9y0ei9+dTBe8/2V7Zfv&#10;1RcRxbX7XbmPX6y5p19EI5/ftP9GPhe33X/kmn7W6ftyvx/R56XF8/HWK0+X/4eAitx538GV5YDX&#10;zG+5bq7L6vMiilhazuPC0uxVvzsR0dPvT11+azGLOxazvtp8at++dbf/x/dOVlESFRAAEnPj3i/E&#10;jXu/UFv/2Uwzdn/54MDHWV44LQAwFm7/6oHYsn1Xz/svXJyPdxe2DbGi6t2xmMWD78/01eZLN//G&#10;utsFgPFhEiAAJEgAAIAECQAAkCBzAIDx8sm7Iu74Zrm277wW8bNnqq0HppQAAIyXxmzE3PZybfMb&#10;qq0FpphbAACQIAEAABIkAABAgswBgB6NYincaekDGH+uAABAggQAAEjQQLcAnnqsz/zQ3BPRbA3S&#10;5YYOP348Op3u0I4PQLryPOLoo32Oe7P7IvLdwykoIg4debV024ECwAP39Lc6VMzviJjfO0iXG3rk&#10;ieNDO/ak2P76tyMiYunNPNr/e7RTPPZ85eFo5HOb7td+6ejAfV1aOj/wMaAKJ7777MpqgD1a6uRx&#10;YXH2uu29/v5Qn0ZWYtzbsjNibnjjXm0BgPFz0+t/GxERSxHRHnHfu+8+uPkfsKKI9rHBAwCMixPf&#10;+04lx+np9wcqZA4AI1ZsvgsAQycAMFJFIQAAjAMBgBETAADGgTkAjJbxHybOCzd144Wb+nyH1RvH&#10;hlMMlREAGDEJgE28/VrE90suB3xpqdpaYIoJAIyUOQBsqrscsXi27ipg6pkDwIgJAADjQABgtFwB&#10;ABgLAgAjJgAAjIOB5gC8+HKff8ybZyOapwbpckNdY8vYMwcAmFTdosS4N3smIh/euDeIgQLAoSf7&#10;XXinHaP/gFrGiwAAlPPehd7XXIiIuLjUrLT/TqfMuHdy9TF+vAuA0TL+AyWdPd9fAGBj5gAwYhIA&#10;wDhwBWAMnf3cX6x5Vqz77dVPVr9fc3/9EzfNxO4d+XXbrz7c2ifrHO+q7cWaTevv28jnYzONfC5u&#10;u//I6jGK1eMXa467uq1Yre+q/VarLoq4tHg+3nrl6U37g2G7876DK8sBFx/1OxJXflZEEUvLeVxY&#10;ml2zz8rXXn5/oEoCwBha+PQfD3yMT906F3ta4/cHJZtpxu4vHxz4OMsLpwWAafXJuyLuKPlJgO+8&#10;FvGzZ6qtZxO3f/VAbNm+q+f9Fy7Ox7sL24ZYEfRGAADGS2M2Ym57ubb5DdXWAlPMHAAASJAAAAAJ&#10;cgsAIAGtew9H697DdZfBGHEFAAASJAAAQIIEAABIkAAAAAkSAAAgQXlE8XzdRVQnezCEGgCGoxtR&#10;vFB3EVXJ2+1TD9VdRFVarX37I2Ku7joAmErL0zRmerUMAAnyQUBD0lg6G9veeO6ardmab7PY2DU/&#10;v2r/a9teu2/E0tt5vPlGvmnb7Lo6Nur36p9f2/aWL30jsplmbKb90tFN99nMpaXzAx8DqnDiu8+u&#10;rAbYo6VOHhcWZ6/bvucrD0cjdwGT0REAhmRm8b3Y/vq3a+t/KSJ+PuI+P/HFr8fMZgGgKKJ9bPAA&#10;AOPixPe+U8lxdt99UABgpAQAKrTZVY2Iy2ufw0d6+7WI75dcDvjSUrW1wBQTAKhOD+N/UQgAbKK7&#10;HLF4tu4qYOqZBEiFXAEAmBQCAJW5fkLhOoz/AGNBAKBCrgAATAoBgOr0cAXAHACA8SAAUCFXAAAm&#10;hQBAZXqbAyAAAIwDAYAKuQIAMCkEAKrjcwAAJoYPAqJCrgBQgU/eFXFHyU8CfOe1iJ89U209MKUE&#10;ACrjcwCoRGM2Ym57ubb5DdXWAlPMLQAq5AoAwKRwBWAMdWd3xLnfeGigY3zsxjw+edOI//P2cAWg&#10;kc/FbfcfWZ0LUKy+K2AlFFy1rYgo1v78ytyBIoqiiEuL5+OtV54e0olA7+687+DKcsBr5rdcN9dl&#10;9XkRRSwt53FhaXbNPitfG/n8KMqFKwSAMdSd3RHvf+bhgY5x661z0WqN3x+UbKYZu798cODjLC+c&#10;FgAYC7d/9UBs2b6r5/0XLs7HuwvbhlgR9MYtAABIUN5q7X2u7iIq1Ky7AACmVnOaxsw8IjtQdxEA&#10;MAEa0zRmugUAAAkSAAAgQQIAACRIAACABAkAAJAgAQAAEiQAAECCBAAASJAAAAAJshjQBLnp9b9d&#10;82x15b11V+DLYumXefzi/+br7pdd2ba2SRbXbszW2bbRfrd86RuRzWz+acztl45uus9mLi2dH/gY&#10;UIUT3312ZTXAHi118riwOHvd9j1feTga+VyVpcGGBIAJsv31b/e871JEnBpeKev6xBe/HjObBYCi&#10;iPaxwQMAjIsT3/tOJcfZfffBqQkAx//6N1dfLGSrLxCy1dcMWUSWRZY1rvl5XHl+11/9pK6ykyMA&#10;UKH1rkZcq9h8F2CidTuLdZdADwSAEpoLJ+OWY/vrLmP89DD+F4UAADAOBgoATz3W5xzC5p6IZmuQ&#10;Ljd0+PHj0el0h3b8DxnE1ucKADC98jzi6KN9jnuz+yLy3cMpKCIOHXm1dNuBAsAD9/TyB3+N+R0R&#10;83sH6XJDjzxxfGjHZnPZuhMSr2H8ByZUIysx7m3ZGTE3vHFvkADgbYClGMXW5woAwKQQAMpwH3t9&#10;PVwBMAcAYDwIAKUYxNbnCgDApBAASjGIrae3OQD+7QDGgQBQhkHsI7gCADApBIASMoPY+nwOAMDE&#10;EADKMIh9BFcAACaFAFCKQWw9PgcAYHIIAKUYxdbnCgDApLAWQBkTOIa1vnZ4+J30cAWgkc/Fbfcf&#10;WZ0LUKzeTln5B71qWxFRrP35ldsuRRRFEZcWz8dbrzw9pBOB3t1538GV5YDX3Bq8bq7L6vMiilha&#10;zuPC0uyafVa+NvL5UZQLVwwUAF58uc+RsHk2ojm8RWq7IxuYJy8BtO4dQQDoQTbTjN1fPjjwcZYX&#10;TgsAjIXbv3ogtmzf1fP+Cxfn492FbUOsiGHpFiXGvdkzEfmoF2fvzUAB4NCT/S680159TLrJCwBU&#10;4wcn3o//d7r+pU7/5Rd2xq4bXcCDUep0yox7J1cf48dfkBKWbv5ctPf/tO4yqMF/+cHp+IefnKm7&#10;jPitT22NXTd6FQmUZxIg9GF0t5kAhksAgD74CAhgWggAAJAgAQD6UJgACkwJAQD64BYAMC0EAOiD&#10;AABMC28DhD5YzXAE3n4t4vvfLNf20lK1tcAUEwCgD0VP6x0wkO5yxOLZuquAqecWAPTBBQBgWggA&#10;0Ae3AIBp4RZAopbPn4lf/eg/VXrMXhccar90dOC+Li2dH/gYZRj+udaJ7z67shpgj5Y6eVxYnL1u&#10;+56vPByNfK7K0mBDAkCiOhfei/axwQfitXoKAEVReb9QpxPf+04lx9l990EBgJHKI4rn6y6iOtmD&#10;4bbGmJvs19BuAUDSuhHFC3UXUZW83T71UN1FVKXV2rc/IkToMTbpA+h9/2RXfGbPDbE2yFz+7vKp&#10;XT7HK8/j2u3ZVduv+q645msU627/+E3NwU4EKGN5msZMtwAYsckOAF//vY/VXQJAJVwuZ7Qme/wH&#10;mBoCACMmAQCMAwGAkZr0OQAA00IAYMQEAIBxIAAwWq4AAIwFAYAREwAAxoEAwEiZAwAwHgQARkwA&#10;ABgHAgCjZfwHGAsCACMmAQCMAx8FTN/yrTfH7t/916XaNvK5uO3+I6tzAYrVdwVc/rz7NduK1c/B&#10;v2q/iIgiiqKIS4vn461Xnq7gbGAwd953cGU54GLtmg7XBN3L60BEEUvLeVxYml2zz8rXRj4/inLh&#10;CgGAvjVvuLm3pX/Xkc00Y/eXDw5cw/LC6dIB4MyFLH7n6I39NWr8KiI7Xao/xt+ZheXSbW//6oHY&#10;sn1Xz/svXJyPdxe2le4PqiIAkJxuEfHu+azfVqsPgOlgDgAAJEgAAIAECQAAkCABAAASJAAAQILy&#10;Vmvvc3UXUaFm3QUAaTm/OBedi72/h//ikj9TE6w5TWNmHpEdqLsIgEl15twN0cy8rz8RjWkaM90C&#10;AIAECQAAkCABAAASJAAAQIIEAABIkAAAAAmyGiATqbltV9z1lz8u3f7NV56Ot155plTbW3//T2PP&#10;739Tn4n3eVlzW+9LAcM4EQCYTFkjmjd+vHTzmdkbBmpbpm99TlefMOncAgCABAkAAJAgtwAAqNTl&#10;+TlFFCsbitWva55f3nLdzxgZAQCASplTMRkEgBKaCyfjlmP76y4DAEobKAA89VifUwiaeyKarUG6&#10;3NDhx49Hp9Md2vE/5FIVQGryPOLoo32Oe7P7IvLdwykoIg4debV024ECwAP3ZP01mN8RMb93kC43&#10;9MgTx4d2bADS1shKjHtbdkbMDW/cGyQAeBdAKa4AADDZBIAyCgEAgMkmAJQiAAAw2QSAUgQAACab&#10;AFCGWwAATDgBoITMFQAAJpwAUIYrAABMOAGgFAEAgMkmAJQiAAAw2awFUIbxf+K17j0cra/9eUQU&#10;URRFRBSr/13XPi/W/Xk2M6tPfcLEGygAvPhynyNh82xE89QgXW6oO7KBWQKYClkWEdnKl7Wb9alP&#10;WEe3KDHuzZ6JyIc37g1ioABw6Ml+F95prz4mXJZFt7ntw+erkwKv/kNTXPfzq7d/xP9Em+ybrfvz&#10;zdoDMKhOp8y4d3L1MX7cAihhaccd8eZ9P6y7jA199ta5+Hxrvu4yABhTJgECQIIEAABIkAAAAAkS&#10;AAAgQQIAACRIAACABAkAAJAgAQAAEiQAAECCBAAASJAAAAAJEgAAIEF5RPF83UVUJ3swhBoAhqMb&#10;UbxQdxFVydvtUw/VXURVWq19+yNiru46AJhKy9M0Znq1DAAJyusuAABGJcsacdNt/ywiiogiIqKI&#10;oihWnxex9P7b7cX3f/HLiEYWUWQRkUVEVhSRZVks1Vl71QQAAJKRzTTjzm9uOPXtb57/4+zJUdVT&#10;J7cAACBBAgAAJEgAAIAECQAAkCCTAAE2sXz+TPzs7/5VqbbNG3bFnX+2+WfH6KP3PqiGAACwme6l&#10;+PXpN0o17fz6A31U3QeVcAsAABIkAABAggQAAEiQAAAACRIAACBBAgAAJEgAAIAE+RwAANJRdOPM&#10;iWMri/xGFlm2uuLv6tdfv/N/bm+1bvsXWXZpdY3gKLIsKzqdKBqNxqV2++R/r7P8KgkAACSje2k5&#10;Xn/2Tzfa5U8i4k+KIruyoSgiGo2IiO5iRMwPtcARcgsAABKUt1p7n6u7iAo16y4AgKnVnKYxM4/I&#10;DtRdBABMgMY0jZluAQBAgkwCBNhEfsPO+MIjx0q1zRoz+qi4D6ohAABsImvMxJaPf1ofY9IH1XAL&#10;AAASJAAAQIIEAABIkAAAAAkSAAAgQQIAACRIAACABAkAAJAgAQAAEuSTAEtoLpyMW47tr7uMDZ2L&#10;iFfrLmJM/N6+iH9zd91VDNe3fhDx6s833+/P74743X2T38eoLZ87HT/+d3eVatvc9rG4669+oo8K&#10;+6AaAwWApx7r8wJCc09EszVIlxs6/Pjx6HS6Qzs+AOnK84ijj/Y57s3ui8h3D6egiDh0pPxLvYEC&#10;wAP3ZP01mN8RMb93kC439MgTx4d2bADS1shKjHtbdkbMDW/cGyQAmAMAAAkSAAAgQQIAACRIAACA&#10;BAkAAJAgAQAAEiQAAECCBAAASJAAAAAJEgAAIEEDfRTwiy8X/TVono1onhqkyw11+ywHAHrVLUqM&#10;e7NnIvLhjXuDGCgAHHqy34V32qsPAJgsnU6Zce/k6mP8ZK3WPq+bI+IXf/RaFDNzdZdRmc/eOhef&#10;b83XXQbAWOl2FuP4X/9m3WWMBXMAACBBAgAAJEgAAIAECQAAkCABAAASJAAAQIIEAABIkAAAAAkS&#10;AAAgQQIAACRIAACABAkAAJCgPKJ4vu4iqpM9GEINAMPRjSheqLuIquTt9qmH6i6iKq3Wvv0RMT1L&#10;+gEwTpanacz0ahkAEiQAAECCBAAASJAAAAAJEgAAIEECAAAkSAAAgAQJAACQIAEAABKU113AuMi6&#10;y3WXUK1LEd1OVncVAGOl6CzVXcLYmKoRotXadzF8FDAAw7HYbv98vu4iquIWAAAkSAAAgAQJAACQ&#10;IAEAABIkAABAggQAAEiQAAAACRIAACBBAgAAJEgAAIAECQAAkCABAAASJAAAQIKmajngoohfZVmM&#10;YqWm7RHl+snziEYfazB2i4hOp0xPERFxMSLeL916fZNy7ssRcaF06/VtjYhmmYbOvff9nXuZniIi&#10;7XPvxsr5D9vFEfQxMlO1HPCotFp7n4vIDpRp+9RjjXjgnt7/2V98uYhDT3bLdBURxfPt9qmHSjZe&#10;l3N37v1y7s69F+N27ilwCwAAEiQAAECCBAAASJAAAAAJEgAAIEECAAAkSAAAgAQJAACQIAEAABIk&#10;AABAggQAAEiQAAAACRIAACBBAgAAJEgAAIAECQAAkCABAAASJAAAQIIEAABIkAAAAAkSAAAgQQIA&#10;ACRIAACABAkAAJAgAQAAEiQAAECCBAAASJAAAAAJEgAAIEECAAAkSAAAgAQJAACQIAEAABIkAABA&#10;ggQAAEhQXncBqTn0ZDcOPVl3FfVw7nVXUQ/nXncV9Uj53CeFKwAAkCABAAASJAAAQIIEAABIkAAA&#10;AAkSAAAgQQIAACRIAACABAkAAJAgAQAAEiQAAECCBAAASJAAAAAJEgAAIEH/H0UAEOKFKefjAAAA&#10;AElFTkSuQmCC&#10;" - id="image1-2" - x="101.64658" - y="144.70558" /><image - width="28.021778" - height="26.974236" - preserveAspectRatio="none" - xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAhcAAAIDCAYAAAC3njzFAAAABHNCSVQICAgIfAhkiAAAIABJREFU&#10;eJzcvWmXHFeSpveYXfeIXIBEAuBSXIq1jGakI43Okf6Izqh/kX6fvo2OpJ4uksVibV01XSSQe0a4&#10;XzN9MLvuHpGRAIhidU3LecDM9PBwv34Xs9deW678w3/6T95hFBw3Q0QZTKiiOELvAyd2j6tyo0do&#10;FY5HR9TZ9DBIoXpBqax8Q+cjVAdZM9CjKMVG3EfoYIuyKWtcFakDvW05ZkBN8KqYKENn0CsjUKtQ&#10;/IjOjH68o+iI4xhg9Gw74X7lOLAeCqsB1mYUqwiVoSj36zVDOWL0Dq/Oyo2u3tPLHUVqPFN67soa&#10;o0cHR31DkWtUneI9TmF0xQDocDG8VE7qwLO7LdWU1+sjalmzGpUOx2vFqGgRNp1xX0BV6cfC0Sgc&#10;D+DqXPYVY+R0hCLKVpxBOgZZ4QKu94g67gVxoXPox4HeN1QR7vpjRu3AtxSr9CYUh1GdCnhREMG8&#10;YCYohZ7C2pxua2gdcamMK6ilsB1hoGOoHWW75WhzhdeBW+8YqtDrCvEBLRtwUD9BXGEcKVoQLTBW&#10;eq1Y3VDVKKsVGwOkR1yQcaCMAyhse8XLCuUIhkpn93Tjlh6gK9z0cC9GFaVIYVXXrCr0mw2IY8dH&#10;jFR0uKeXEdji2jHKOVt6xjJSZGBdb8BGnFOqrqnaIWKs6h3Yhooi3RqRjoLR2T2bamyPnnOE8PL6&#10;kv+A8z8Vp95c85fSsT1aoT7ydBz5tCrDyRP+z7LmaxHuq0NRRnGqACrETC2ggoiCgiEgggsUYF0r&#10;BcNVMFWqKIKgInSilFHoFMBxNao6VQ097jk+O+Enn3zEZ599yk9/+jlnz58hEs8tpVD6QilKdaPW&#10;inlFBJBoA+6oK+K896EO4uASf7sIyPIKz38gIvtfx0VxjxuoKEUVMccHx4aR4X7D3dUNV68vuL64&#10;YryPeSguCFANzKZHRD9Z/KGi7Uw8I38HiD8NFUGQaJt4dEu21bOvpIAWRVQRFUopnJyccPr0Cafn&#10;T1k/OaY/WoEoFY91LE51w8WBvLf71NeaPw3hXbq/tX/6XQTBcXHcbepfyT50YHu/QaWgonh1PN6U&#10;u9t7/vDHP/DtN9/y7Ve/5fv/+or7m3vcoEgHLngFEUVEsOhBTJya/7V+xBQ1YfkSnp8eei/HD3+w&#10;947v3B8iO9Ptwf1l+uDgNf5wSv7oh7vvzP2d99xfLgeOv2Z9vu0QDj8/5AiE0BdUhXEcwaHrO8Zx&#10;xN3puw5x6G76LheNYAadQBmNXjsw5c6VG3G8KE5PGYVRCi6VrQQY6bwwinLbOS4FNei0x2uHlA7z&#10;HrcRLYJXB+sQOmpRxk7YiGBmlLGjaI9RKZLKWYWxKFsEHTtEYBRnRcdqLAhGMad2PZtVx02BDih1&#10;oGNAVagINhq4I9KxKc5FMcbe6YtxNAJVqF7oUFQMuo77csIoI27gKMqK3gulGu6VKgN3xbGjng09&#10;F+tjRFac3FdWKoyyYpSKqOEYMgZA2KCMfeG2NzY+sulASsetQamOOqgr6oJScTcMGAtUAHdKV1j5&#10;UawEX6EOo3bcd0rVjgIcjY5Uw90oFHovGMIGZVsKl0VQrRxbRycw+IhtB3TrjMMd9x5CXUaBWqil&#10;o3QdZTCkdAy9st2OHHvPWnsGu6L0QnXYAFLW+GrNVitqlW47cNIX7hW23QrWa+iUgYoNxlMTxDpq&#10;f8zdaoWrMxbnrhhWjeNRKVa4xxlKRzl+EoJfKoFbjlB1xrIOEOVrRi+MqngRVI1elFJ73AuDg2jH&#10;5ugJygnjMKBFKNqxHTf0655KoVLwraFyxPHZU76/v+Iv9YbfjSOb0uHqnGxGrq3n5dkJW3FGN0ws&#10;VqkqmgrWmAV+W6wqghNrsLhTVFDAFFQAjdVeAMXpOkG8Yh4g/+h4zdHzJ/zkZ5/y+S++4IMPnvPk&#10;9JT1ep3PrJTSxe+1Um2c5GopCijmhhnoW8Xaux0zsNiVVHE+gcUjz9JUeI6gLshoDJstt9e33F/f&#10;8uov37G92+BDxcaKGGAO5rjndy3eJ1BHgCazQD0hxwO8uIR6DXynNODTBL9ke13auAmigrsw2ogm&#10;uKjbkXEzcH11zfHlJWcvzjg7P+fo9BjtCqbZLlEMm/q/9ZM42K5GfPQQkUk5NaU0K6p41/a5pGyt&#10;tSIilL4Dy3dRTYDpHJ+d8IuTX/CTT37Cz376M7758ht+/dU3fPfd9/hoKCWUtjUg46GEfAZsJgbe&#10;RlWyKU1j786D1l73uf/b34fe9VAfTF31oA/2jr2vtxb6Lrr4b+fY66u/x/FYXwq5QtyoNc9pzDGR&#10;MIBIIN+d1HtMCngwFeKO4sjoqBeKg4R5BW6o97EoXSleEa8UESwnm4mgQqBiMaiVtYdyDoELKCgj&#10;vduEZntXegcdDRPFcsIogtqIOhQviIXyF3FGie93taOriruydqVALDAxnIqIozJGx7ghDicAQ0FH&#10;oa8GrlQETxYHE7T2rFGgIgspWUP7s7KwlkZZ4dKH8nZn7OL5nTtrM6iWbQGpjrpnXzorFcQFqyFo&#10;qgqlalqPqWQEHGMUx0ToqiIGhsa4eVgJirIGqqX1aQWxWVCbO4ahGCLQN8E2OpvRuakVG43T7TYs&#10;z66kFRkA8sg6TAVTx8TwQVj5CrVCNYNO2TBQtYdeWJmDgfWgpqy1A6t0KDlZqCOYKkUFrEInbEoI&#10;gPXorKpSigCFdY3pve0d94pUyXExxA3xAgQwdgczo0NwMcRgXQudQQ8YxkYVN6NsBXXoPZSv9MKJ&#10;C+utIhRuvdKZYQVeD3ecdB2rs3NW4mxFOHbj6dNTbopzta78fjtw1UVfeeorkZzLBMBWnOIyWckg&#10;wf5gSFFco/3gMZ9xVKBTDWDrlfXRER9/8gGf/fJnfPTZx5y9OOf4SSgzUU8L2XP8m/bKZ7XZbJ5/&#10;Syr1HyZlDwohnRVHO5bWoCQzEWtcKCIkSRDjIU6hYBU2mw3XV1dcvr7g+vKK4XZDHcYAFO5prNAk&#10;XgAMwnJ3a/ayxN8Olhb9LMEbi9B6QRbtnAFGU9SIIJqfJRuBxnnVAPPjcMH9zQ1Xry45Oz/n7MVz&#10;jp6eQGlCGFBi3SaDMSni7JNDSnWpkN/U/+7JiiHYhFgk+yF+rQFnocQacnG0L5yePeEX//3P+fAn&#10;H/D5Lz7j119+w29+/Vtef3eBO3Slgyq0biyiFBGqV6rJbPYu5sAEhNgnDeKMLdT8o9b8g3d893n6&#10;qKI8AM4C7NBQ5Q9+1vseu238YWzNo6DqPZ//prtJskLL8RMmai9kXZ7tVlbDGp6QPJjGapVUgOoB&#10;OsJu1gm5iAeBp2kZdBYKXUSopDXpoBZCzgRqEJeIQ28GVEYlwUM8Ew8brSY0llTu0Y6g/C3pYDGh&#10;M0GsTd6gMcNtohMyFhx8xBgxFxSjqxYULgGWGnUZTZQAMxDK2EHdGTVYBFdFU7lXwm3SV6hSQdI2&#10;ye9orsQqsZTUHQigpTjrGp8NaqHMSaC3GG4ntZRDMUE9FLSpgCtKPEuqsPIQMJ5y10qqDasUlE7A&#10;a8XHSh0r48bYmlMRtBrU2SoBw1MtdhVGYMi5FCBIksY1XJMm1WRfqk3MkgnTPMGNAog1UBeHi01j&#10;oOYUyzlhIFLoaoz7fR1ABCMYHhFDNOcvBUaLSW5jnLcAVJ0JXSX+FqH0ofVXXtKq9GBBqiHudBbU&#10;31EClWsZ+NOwwV0CDB2fcFpWnFZDxy1/shsu7i+57o+pXYm1JOCuwTZ5kBB916Fogol5iRoxnlVB&#10;+o5OlY5wsY3jiNtIFadbd3z04Qd88fOf8dnPP+flBy84Oj1COkWaHbBgCKAprhlYzB80O7NxFq0l&#10;bz/eRai5+66obAJIks2BcA2YhwukFGwwtnf33N7ccnFxyfXlJXc3dwzbAcwCUDoBHhrAaFaKJ7Bw&#10;WwCY1o7F3zDP8ek1sm9k5m+kgT+VSbCG5R8uTm/+HwkZoiJ0LtT7gevhku3dlvu7O56+eM7ps6es&#10;To6oGNUNazdCpvEyYo5EH+0CjHdVJHFNWXxXOPS1ffYE4uHaF548f8IvT37B85fPefHBC776p6/5&#10;8z//C/c39yBC13eIK+NYqWPI/L50wYAxg4mddi3fg8RR73C86Z1/bMV/sN2PAL2/5tlvHsdZfb/P&#10;8WMBjqk1B+7VZEVb3TvgxGMxdpWOKh0NvVcxhuIUN1YJQ0ZiAjqG+Ii4YoT1KjhFUph7LPLww4Wi&#10;dTHGZjs5YbW6U5rSFZso4G1PCo5004gxpgIVgS7VbrxOmWaniYVQlRDMY/GwUFzovNC5pB+4glZM&#10;DJPKIM1aLLnCNH9WXGOSuUKhoub0VXA6SjI3o3aYlLQtBUk2BocqzqYLJbsahc4CQLmEf9wEBg3A&#10;1JkkuApVPmiAjXUNS6YmXasuFJt9s6MGzaommIXvNvqvAQ+opVLLiDl0o0EVdAM6GNtxZBhHqhnm&#10;xpEEUxJDIAH+EnQNCl1ai6pJ4RNCtarhUgkVpagpijGUARdj04OYMmj03age4+6GUClAFaFKgLl4&#10;Ltz1Gv1iSvGMs3Gn82CPtl1F3OlTgdWc3+pdWk86GbWGsJHCqNCXdEkoIMI2lb+j4EanQsW56QUt&#10;oDXm9KU6925cmbMypd5vKIzcmuA+ctsr97oC61gPgqsmwBBMgj4MUFTYSou5iJ9eNBiLThi1IgXG&#10;bcXu7ulr5WTV8fzFc85fnvP5v/uCFz95ycuPP+To5IgSoRsBWi2WcngEJn7iMbFx4HN781faN98g&#10;vIJxYbLol1eKNKbOU3ZEHI1TGTYDd5uB+4sb7q/vuL665ubmmu39JlyTHjC3gYZgpxJgmIWbyAGv&#10;k8XcQH4DFkZcs4Mp9ntE0gWQFwUolpm9mN4jPhMLF4OZUSSBkwKjczfcsN1uubu94+zmnNPzM46e&#10;nKKrgpYu5GiyLEjEmAQbd1i5vJNCSyMs7uKP3GkPVMxnkRLustKv+fDoA56cPeHTzz/lt9/8jt/8&#10;+lv+8Ls/srnd0EkfbtJSqGMYJV3XYVjK3zbmTVS3EyxalXJrCYR33v5RiDLd+8Eb/AC9fBA4HHjs&#10;vitFmlGa82vn+e+o1w8xUQEMDlz7Du/0vnBify23/g8CSvbOPvzubIj6hNbVhW7UgtFNA2xJ35sY&#10;VQVzxdBQiGmjaloGDcJXQgF0C6sAMrALp+rcvmBDwjpqFlYSdGGxTe0PaxdvFml8btPgatDgYsEW&#10;EC6QUWEoIVhKFdxL3j8UWjTMMA3FLQhicS+xDhcY1dKKnbu1SOI0N1wKJgp0mASrIO4BQtLVg8BW&#10;CDgkYBJWjkkEUQUICkAWjExY4cE0EPSu2GxDegCUuC6NJW9MQLqkcCCsJxGICAuh+oBXow4jbBy/&#10;c7RKBJel/7TD6fNhNadpMQiGRfFiwZbkuLb3grD2G+NjzZU2Aakcx+IMVRldqMUT7BlKjfs1lsh1&#10;6sNRo980WakxUdXKC4M5Qye4OtQktD0oVi0le0NQd9YEu4IGUN0mk6RE/7k0d13J2AjFbWSjhqiy&#10;AlwKW12xVaea01WhmILDhTquhaEvWBFk1FAS2kFXoCtI0fi9FMpqla6P+EzbNX1BSuFoXZBaGV5f&#10;4tc3vDw65meffMwXP/2Ulx8/5+mLM8pxBytNIW4B6xysGkhbcG8TN+8njt5mFU3+fnYtwUb7KyX4&#10;T0vOy0Y2d3dcvL7k+uKSu8sbhs2WcQgWiRTkuGO1BlhIcFHdMUuWIkGNWJ2ARWMqDJ8Ax8MGzz0R&#10;77aIuchzk0tk8U893JQigpihGoHTjtGpUjKwrd6PXI9X3N9v6L9/xfmHL3lyfsbR6QndOmTI6BEQ&#10;KdJYhodxFcu+feP40JiUXRS17wJYBt0uj1oriEc8SVFOn53y5NlTPvz4Qz774nO++fobfvvN7/jz&#10;P/8L29sNq7KiWxXqtlLHAUljMN4nRNTynaZ27n22fMf2+eNz9OH1/xrHfpzQBDB2zjGhnsfatmPp&#10;Pxjjw6DpbfdZPvtde+TdGI7DwOLRe+a/boCwiF1QHyjm9FJS+YXl2tUOMcPKTNsVoLfw5VfCxx3+&#10;/fBtFoHOalpq4eLwZDpQD5dFowMlFOyqRqdsSzAozarva1DW2xIWWYqmWNho0rge8R8TClG6GlPB&#10;VKgiFO+CEYEcQQfTePf034/qjBqsSWfQubV4ulB2CKMGk7CqaUVL2sap+JubImIAQC0AVJWwgNMV&#10;HlhHAnyptRgLobOg5SVtblJxNGAoImFhDznkydqgmlZaDWBQnTIYbkRA69bwscLojDg1LcsuAzex&#10;ORq+LSNLNqvFfiS8AJoLP9we7lBVJzAgrnTWUcySgVK2JdqrHrSzWqHBACQAY0kwK8B6bD0fzxIk&#10;+smElQXTUyViTSDmMIAOW0SMWmRyb5hEPIt3SvV0T9RgKqpYsAsUqiqDAXRUa0DWMSmYdhRVNmNl&#10;VEGOeqwXxk6gi8A+LYW+O8JKQboO7TukW4KIHlmvkaJhuapEsHS2TSWCj7uxUvoVxx+85Iuf/IT/&#10;4Zc/46MXZ2gxnDHifjyihEQzgNE15sC0yg+7N5x2zeHP39cCakfXWD/fU2iZhaIaI2bmbO/uubm4&#10;4vLigquLK+7v7vFqMDrmxLzMVqevc2YlkrGI4E2jpgA0s2SifHLLzEwGExjff+fJ1l+YiUswMYEK&#10;mWMiluc9wUanCjXjmzTZA4NxNDZ39wzbLbdX15y9OOfp+VNWx2s6DTaYygTa3/toAgaQRtOwtIhn&#10;w+ngWIuk91LC9ZPA7fjJEV/88qe8/PAFn3z2Cb/6xy/5zdffcv36KtbuqqCmzd5JlTSDoWkusFDK&#10;j7gcltfvf/ZDrn+X402xLO9yzze5SP5q98kjX92JF2EBaPYA5Hs/V2Shf3bPt8OTKZTU4Y1va/FN&#10;nSfl5+6oGJqm81acrVpGYhcKgkkKM9LyhIx1iNdziaC6UIShIF3TakTCN04oZEfYSgmLQpyVB4Pg&#10;0hJNI/CxM2VdS7hrdIxgKnPENeMTwuqUFKSC0FVBTVCzZBaChalN6RsBSCRiP0KAVdzHVHQVxShO&#10;um8a+9AxSvABmt9vwAFacKEEWLIIBoQAFCaOabAWxRvdb7MgiAjK6FcLd0GbWJ4BkMXC2g9akQQg&#10;AT6ksSN4AMHRYFPxwdAhc/MsqP8qma4ms8WvdJGeRoxd69cdy0YCbEajBNearg1L5qQLHaAZNT4W&#10;ihf60YO5yJgSajBFpS0cjSC3bgwGx0qAtz79HENrb7IP6n1O+hoMmMX1ksxRl24PLUljNoqsg1pg&#10;9AZIol+rWPRJKVTtQFZQlL4rlE7oxZFS8K6n6IojXaFFGNZQ10K/6iirFUdlRdd1VMBKgL0GHND4&#10;W1QZLRePFlwC0I3SuBboVWAllL4HgfH5UzZPj7k/6TmSDFi1iMEQi/ViBgOGauGtimk5jj/yMcUl&#10;pIZxaQGbwQhhMNxtGO823Fxfc3NxyfXFNZu7O+owZqCg4hbz2y0U2wQYEmw0sFDdps8beLBk5OZU&#10;UyYw0twih8BFm0WWyncKtZVQshOIkDkLY+f8AmwYFcYasT2qcR0RP7S5umXYbNnc3XN3fcOT8zNO&#10;n57SH63RrjB6xKu1Rh7OCnmXY2YA9vXbY6wFQCdd9JWlW0kMKUTwpzin56f8/OhnnJ2f8emnH/Pt&#10;N7/lT7//E9eXN2l1RzzRI9j2b368mfF4NzZh/9xfG1/xt2ZXpnbCpOT/2nu99br5CwFiswEikUHU&#10;RbaBYlIZ1SkSQZImjnmNUL70c68Gn+IVTDIbBEHTTRtujWAhSLdJFWfUULK9OataWYXrHy/CkM8Z&#10;RYI6ThahiNN5RV2oaLAJOkJS/eodWNSbGMsWk7B3xYOuFhzXIRBVCrZRezpTjgZFvS6CVytVjbFE&#10;aOEq00EbvQ8lrb0VBaHPGhrogGvL/ujB+0jH0g1i0FVwEUYl/OhSI34gWYqimb6VynmaEGLpPgIX&#10;ZUhrVMQomVFQxRmKpM8+QWGteIXtaNhY8W1Fhko/OuIR02AqbHJWqCXvk+3BoSAUH0PZFUvoYhMQ&#10;MAGTkgG5hOVfRkAotopYkjIAgtgxXS30dUAZEbZRq8LWqCkmPSbCoCOI0WtN0JiuCifHsYFNwUS4&#10;71ZUBJExarOknlRGROB+dJRg20YVNr1GFlMJn3bXr/Gup65WSA/aQd8VunIKZYWVDl8pq77Qh/cB&#10;KQXrjnBdU2wFnaDrinTBjEi64DaA9MF8eUbMt2j4FiboZNoWpWm+dLsFkDSErQr0sKlbvr58xc3v&#10;t3xxd85Pnz/jWV849ggcVJKK74QB2JjRCWl1vEHELOSH759+T8k0CaV8p4hFSCu1GuN2y7AduPz+&#10;gpvLa24vr9nc3GFjnVyDWKZET0aTByi2dImk62MfXCxjLKot2QomJmIZ4Ll8T52s7PigGQPtnVQF&#10;t8z1EUnGNDIlAhgvwJOm1Z/nSrIAXfNWuaNFMR+5qVds7u64ubrmybMzzp4/4+TpKbLKIJrFIP0g&#10;S9onR/AOqmiJIy3+4bBVLBHQTEk2pVKrQYHSCa4SQeGrY9ZHKz788CVf/PynfPPVr/nyv/yaP/3x&#10;z4ybkMXaQE2yoUvXVZsnAukaP3zsv+9j77/DOiBvjFHw6X+PfH6gPW/r/10AcRikPKhL8sh93nUB&#10;TozFj3w85nrbDdrMX9QfXCOknNVGBopP1LNkuLK6TGmTEc0dBliVEPieCFGTa7Ckz4MZCFowhKUn&#10;arfIAshnFjci0DsshZoUmlhQ7SUbPKik62Hu0IiViLTIxqb4XndHkZcEI8j0USEDBFNJmmamg0a0&#10;QalO50uzPTI4BKVz0Orh2vGwqIuH9RiRC9EaXchZk0hF1WQaNBd288wwsTWtp9prZAxGvpabxn0y&#10;mr1lVhQX6lhha9joDNs6BbkVjyBakSykJZrpkdmLGTdgXsFCWK+yLkMTRsuFOlOdEeiItBgNDWZF&#10;YOhawZyw8tQLakbJLxUxRAvVhaolKH4Jt4o4jKVlxyju4d7wZLKMwr06ozbjv1BE6aQgXbADqsd4&#10;d0QpwSr4ukO6jlWnrLSjkw7pe8bjNayVUpxOCkWOEe2ofaGWTGGmglsWs1pT6Rmsw9TZlhGkclwj&#10;Hc/pEnyNyabt+j6bL1XzPZuboM3cMgV8SiS8SIeu4MIHbr9/xdX9Hde3d/zy5Us+ODnhaX9ErQNe&#10;A9xFZGdQktLm2AHp0+IF20eyaGB8b86WsAPfb/eYEiV8FjwtqFhc04Lp8Grc3t1z9eqSq4srLl9d&#10;MG62+OhQLV2CRFxQdVzBzam1TtQrHvFOuGUwp0/KyjKYs7lCqmeGbXONTK/nOz/bBxlLOQF8l2wD&#10;aQXnGpXMhGt95uoUk4nZaJkj1QHN4l+iVIvA9qIx9uNYg70Exrrl8n7D3fUNm5s7nr14ztmHzyhH&#10;fTy4pXRKa08oqdbvj43NTrxF+8msvN6EUbqux6sz1hHtlK7rGG1gGAbQiJYTB4qwPlnxk88/4enZ&#10;U85fPOfrL3/Nt1//nqtX14s5FY2dgyCj811kSkcWWQC89r09hf4uwGI5ro8ef4VZ/64ukL+OrXh/&#10;7uHHYkkmHXto/T+4/WKCOaFHqMj/9g//4K0+gEgFHLeIcTBNlVo71ByPvBEg61poQIvI44DIERGK&#10;SSrLmsoxfezudGahDIUsxiVpvUPmH9BSXIP9UJwypQk2xK1ewLs8P0wsiiyrC0qd+iEi9rPC5ZgJ&#10;tdKCSm1OG4QsYgXQhH+0ziV9xu4gFZMx7551QiQoac/zJQNEq4TtqlPbM421mWqNH3DNGIcQZJpu&#10;gZn5ybblojSDWkdsqNhQqeOIjAEmIjYkhnxVSTbJGUWieJlZVHsURapN9K/ZGItajTGzKdR98n2L&#10;JwuQjJVKpZVkKh5R9q3+iNZuisMxccYyBtiQrAnrGrE9OuJudBYNHlOodmWFFqBLWrkUKB0cHUdR&#10;rC5S4rpuRd91EQhUCrI+gdKhCtIXrPRIUYoS8TVa8IzDidgFy/S/yJqKaprNwqqTEjU6WmZQgMaa&#10;7x3/IshVch3kWjugBGKOz0rdd65V8BL316yPIiNKZYVzIsLHJyd89vwZX3zwkpdPjjgWQ30ERtxr&#10;pkgL4pnaS7oIJLIaqgOisdqSIYuA5AwgNA/3FTKB/ql9yJT14skYiESFSneHsbKSgrpSh5G76zsu&#10;X1/w+vVrbq8iUDPKxnrEF5hnJgh77o2MpcgPLdkLT9BgC2DhbtQE02YtfoYEF0xsRft7Ft0LF0lz&#10;58DkMZrAl7Q4i9kF0taL5FoQjQyRIDA0+jfdISW/P7lHhIjPyQJcItGnpesoq46TZ6c8+/A5L168&#10;YHUcqaujW7RLMxMs+0bwNDZiLM2SIhGZ37O9vuy+++EjM+gsY6G0+dLTgJKWPRPiWUQoFNzg/v6e&#10;u5s7/vm3f+LLf/yK3/z6N1xf3mTbwEfSrNRgotL8bEbSMgB3MognYDEboW/NgvJlquv8njvgJU88&#10;nkvz/oe3Bj/y99uOtwbsvhOL9RCg7PTa5CuLBbDMhhHmtdAsokMtEjzdIfGsdlX835D/9A//u+80&#10;JBfuMuVqepE2WL77kvtoaf77IUKWvEXOt93P9jr17QhsXzy/+Vjgq7kN+7SbMKHotigk29KUxBwO&#10;N6GYHQvxMV+mtMozec99CkycqRaIQfiqEUQKWBTowR1GQpBmBohNplrEZYTLoWVbEGyLkQARahFG&#10;DyusuERhLxXoYLRwTex0b+uvnJ4RnyOzFTT1QjAVNX3fMvVLxB0EQxEgzUUpXUG6HukKpY9gSSmF&#10;1ckRUgpFO7TT3YDIUtBVHy6HUqAkK1MkhVRk8lgzRSHNz7mvVef6Em2ewrz+l/MffPHNNy/6H+No&#10;xdMgXIemC4oeozPjCHhahA9Pj/n8xRmfv3zKy9MjVjKCDYiVNLZtLvhEs2gzRqrVcrDI/lK3GVwk&#10;kG7zOpxScxDg1H+p4IP+zyTx0fD7kfurWy5fX3Dx6oK7mzvqMIAlyK08Kqj+AAAgAElEQVRMYMEs&#10;AjdnF0elVWiaalVM7ERM85Z2Ov30OWOkmiXAYwITO/EXvlfZsnX8UoDm3GkKbg7aZGIplueVBhSY&#10;rmnXzeAianjM2SjMn2vLLtIA072yPj3i7PwZp2dPOX56yvr0COkLo8d8mLNKfIpHEVoA5m4Y3qF4&#10;jTfJ1n2l/Ph183oSCXeKVaduKlevrvj9t7/nq3/6mt99+zvuru8oErUxas1qqQRbbg08SWR6yVRt&#10;1Xee5bQ5zONClgDKh97zAbh4Sz/8Nce7unMOHcsYnsc+f/t9H+rGJbiYGawFG5fPbKzFW9sJ6J7l&#10;1AJBJdTJbgOWi+xBugxpwuE/4AX3Ormd/lGOHzYx5MAfB6Nfm/AgVae3TotjsoQa2tsDFo/7+5rS&#10;nt0Gy1Y14JWhhrSaiZr+Zh89FvBQoVHI1SbrS7UL1wOLc55Wss/PiGvTtWKNtVBqjnFHC8qNeg8N&#10;vbpIVI50T+U9K2kggneL4EcrrBO6rqP0HV13RLfq0b6AKv1qDUXRUtAluEiwMIpjKqiUSDWl4Blf&#10;4qoMWem1pcM6yTq5Iyj9ogZKG9cJHogwLvy+Iosx2P0xj0qzAv9Ggmj3aPlYLcBwIRI92LehGq82&#10;EQx4fX3N1fVTPn35jA/On/Ls+ARVQbyC1Sjihmc8Usue0MlaiTiEHNsULObNTddcpgm2s9vMmtNK&#10;6DQDgatxd3vH7c0Nl99dcHd5ze3NHdvNJgAMElT7OKc0V5sBinuUTHd3sFbyvgELAogs3CFTtsj+&#10;PwiAsQATb6PVl8dSHkRQ5SKoMxX60ifdYjDUbXIFNZ+zaUU1mBRRjcitxnSI4It6HKrBHDmC2xi1&#10;Ma5vOXpyydmL55x/cM7J01O61SoDPueU92xgkEFZ5rv5vt0XisKZLdY3dsXb7flD8QOiEWPSdx1H&#10;R2uevzjnpz/7Kd989Q3/9I+/4s9//DP3txtQohx9CBNwWHWxDUXNsYc5ZsMn63q5Gt4Ajg6M56w0&#10;D4OMQ9f/mMff6r6PH28HJu9y/uC1TXPJw+/NrN4ELhZNOoByl19stMn/H48dMPWO1zWqcef3t0wg&#10;meoQ5Pea8Etg0kmmlNZAlrVG9ocNNQTxVIFSJgqrWaERDe8cdpbNzyulQLUoviWZDWNR0+KuZo67&#10;lMx0SJOsCHRKd7QOn3LXR6nprqPrIuXSi+InfZT0LgXtC123CoukBA1M6WawksxGlWBsvETQmKsk&#10;e+6z5ZwDE/U9Pd1QLFjSCOSqoy1SGOcxO2QRPBa8tPze3+dI+OkzU9YO7XtUhFpHXt+PbP75e767&#10;vObTjz/gs49e8uL0hCPtKFIyJiPqwHQJpGarvrFIFRelVW/WUiZgPcHonFINqGoWaaIaw7Dl9vqa&#10;19+/5vLikvubyPyoYw2LOuiDaUxa0agGLhpr0UBBxJAkk+EepfF9DgZcBm+2oM2d87UeZCyWPw8d&#10;BwXl0qrL/tkHF1OQ52J/BcNiLyVv4KOBiHBFFtWMTZ2DUd0jCLZ0irpTtyN3l9cMm4GbmxvOXzzn&#10;7MUz1idHdI2pixkxZdnN49XGbjcOoCnXv1bZ7Rtly0yZatH/ZV14+ZMXHJ8e8/Kjl3zz9W/45qtv&#10;+K9//hfG7RaVPia3gVdLOBtGVWOVJ3I3gUa2epJv79r+x1jxtynbvxXI+Nc+lsbx/vkHf/tD2bnz&#10;NwkwFuvjwbOW4GLfrfFoIw+wEf9/O6aJN03mmZaDw/2zz9IdYi/aNVOxnwPsj3jEBfjoUVq31lh4&#10;1aNkdwqgCFKdfatBMEgI8Eeok3nBRKCnOxnVLdTMrBjXHU8//wTvO0ofbIOWAA3SpRui7yLQrOuR&#10;TtGui7oNXQk2I4WCSLh32k9vEqLoRM1PwWdzI4O9sabQdJrInu/ZhPJScC47XVrkMQ+FyNvAxJuO&#10;HytY6u3HDqnd4AANZoxG1BWQFd51jHXL7c3A5Z++51+u7/ni5TM+fHLM+ekJR6WjEGnZUyBxBlpP&#10;EUmqmf2VfR2USViNafG1TKyoiaNYrWzvNtxeXXN1ccnV5SX3t3eMw5ggIWM3LAMxc78bHGwMkBxu&#10;EE93h09xBJ6UeQCKRZEsT8//lB2yCxpmoPLDQMXU097mWVuLu4B0H2gFA7ELXEWCXQs3QZTpVwnm&#10;xUsyFWJU02SY0m1ixN4koli1qJBZClJhc3vP3d0999d33Fxecf7yBcdnJ6yPj2I3So8y/giUrkxA&#10;ThIezm4CmYIqZ8m2B7YlZlxbIm8DY4cYDAQ0tu1lGAa6k8IX/+4LXn70kp98+jG//urX/OF3/8yr&#10;7y4YN2PE61gwW1qi/WYRv9eiZKyB4YVx235vRsdjgOHQO7zrWn7fNf+vz1Q83o7laM/Hgs1lVhn7&#10;rd03uBNSH7Rd4/McHbMAFz+oAxZ06r/V422odXq3R17xUJzGjmcJ3uIeybREX0xCEhy4M27GcHuM&#10;I3WM3P+YAElHNZTigMyWmxJ+X2eR6bGgqZrV9aAtIuFqUEVOTzj75U8Z1z1l1c9VB1VAC1Y8drrN&#10;d6wCQwreqP/hlFaKPCTvIpBs9tWH9aQ7brIotS74mHtDZEAt6f9vQrIXRdveCcshzE6vPhdUOoTM&#10;H5u7++cfZe/+hnN/LlQGrQbKftaHpaI1F0xjnLZ15P5+5Or+NZevvuMXH77gF59/Qv/kCSst0XfN&#10;wgTILKq2j02MU2Q/1Rbgm+9bkHm+mnN/e8vN5RVXry64urhkc3dP3Q5gs/vIfd6VtMmLcZzZDPdg&#10;MKLCZnunxlwEuFpW3txlKmbGYt8tMrlKsq9+qEukzalQaT4BjOU10hgUmfvoUL2LVoDKW982N5QI&#10;UIPZc4/5rRD1ahI0m2GVYDE0HrS9uePVduD2+paTZ08is+T8jP54hRaN8HmjoUImF0g72rm39QOz&#10;ODs03w+tkx2QgVN9pKjSH3VTEb+TZyf8+//x3/PxJx/z+9/+ga9+9TW/++b33FzeAEopOs2dqc89&#10;7helEpPCmCw+ZtnBw+ySZfsOtf1fw1j4e4GMtxlQBwiKOE+el+W1h/vpbc944BZZHm8apOUD/xpr&#10;8O95PMbWvAkU7Hz/kVeeKiA+wmboEng0atgcrylI74fY9dQiPbRlwLT4gIZFvUlBJoMfJOjW0Ou+&#10;89xo8/wdLCK9JVM+vRTWT05YvzhDVqssox1+3Ii9UCgeljOSW5hk/LdAIcqxa+vbhRCIR7ZAuMW7&#10;uEcfEAWGTGI7efcITKPNLSHdRM2Snsdv2nIrKZzRbHr2m6yY/1YBsk1ccBvcZC8cWuEvLVF7ZZSI&#10;i3HN/XHceX19xc2TE8Zao+5DZsMokcopRZnoCSLFvMXQiEdhqOJkFoKCGXU7sLnbMm62vP7uFdev&#10;L7m9uGLcjtP+IBiMtaJdoVpmMNk4gaXW38tMj0rM/akYlrfskQVgsKXroLXap31FmD6TxecLYMHc&#10;lW87JuFKm0J5t6R6J8DRbpnn1ed6FyqzfJxiMtRRcrfjVnuHZDK0cUmx1wotRd6C4em6CGzGwIaR&#10;m8srNvd3jPcb6maTu+GeoCV3J2ae90vW5cFakPkdWACynHK7x47xJA/PpVxpRoOYMYxRdwYJV0/p&#10;FOucZx8+4+TpCc8/eMHLDz7gm6++4ft/+Z7t/Rgu3+UzFkerR9LafmjMphFzPyifG+2/t8J2Pn8b&#10;eHqf418TZDT5eLgde++2BySA3Kl5vlcYI60PbOcLO7dLCiTku9K9qxXXzj0mrP+tAox2/C0GfUL/&#10;i3NTGW/CCjTLCOuxMo4jOkZqYLNAGpyY1rNH0GIIsoexGzO1Gy2QSSjOQH+iFRtElainQV/oTk4Y&#10;S2FMiyloZk2wIHSiU4SwIIg1FiZFvvic/pjzcEpRSuEbQWdzDzU1h88Bo3V6lxbcmrEYHpM/cQd4&#10;paK5aFJw71mby775tzBPd+O1bWKs4i9L8Bhpv5b+ixDiipvy9MkZZ0+ecHR0FNZgrRh1Si33rBsS&#10;m2VFwaQsbQMebrmS889sy/3dPVcXV1y8es3txTXDZosP4eooGqBiGMbYnVSE8W6IfWsAt9iO2xcA&#10;YXaJ2DTO07kEm23x7AKLABd15+9Z/jSD3RdrZerTt1jfsKuQYRm4zbyOJOb7pKw94igmkZtKTVMY&#10;N0Zj+dzJV+1zlc92XVVFqBQtdFFilmGoaK1oUTTXZb0feT18z831Da9fXfDsxTOePjvj+PQE7TJz&#10;R5jWrSzew30Zr5HtaoD2kBjcO3dIUUpzWXj7SyhS0BJtqXXExCIzZqVot+aT9Sd88OFLfvnvfs5X&#10;v/qar7/8Na/+8opxY5MRIRMCiIFtpQH2mYp9ACD5Pm+T6svvvS326sfQEX9P2fPoow8AjMe+7xNS&#10;f8PNMjW6awu7lDLReHGjNz/t0HU/lILcv19La/tbH+/6bvvHtGna3uLShfKMdMhc3D6nr4Z7QlJ4&#10;Qx1Hhu0QNLF5pumlO8Gaum24cQYRzRKYWri0OJbWehsfa8xAXi7gGtHsKLSdWeg6BoTj8zO8L7EP&#10;RwKFkFMhPFraIGTNjbhremji5yjgGu8SAabJWHhsOoWyEERzoSYl9jgxmQuoxbNTsDRh4c26yTxr&#10;93iLFEQP4jimvjsc1Lkcy+X47p9ffr4/1/ctxTcdb7r/vtBvcT/RV5Yl2pt4jcj6TgoiHv5tMV6e&#10;PeXD5895so4dU6UJBIWRGtS7tHTVGDd1oQO8htuJ0bi7veX16wtef/89N1c3UfjKBRsr1IXVktmj&#10;LaOjmjHFRixqUTSmIXbNDOYhgEIN90hzi2Tt62W8xQQkPAJBmXqBBXORZ323b3/IeACTa2X5WSg7&#10;mee0SNbKSYYxr51TQSNDZGI0zNESG+GVSRZoKF9r6c9EAcEsVGUJ9JR535wAM5LEk1DvtlxuX3F/&#10;fcPtsxvOX57z5HnsV9KszYRCtADdVvxwliWJypoR474zCR+Tl8u+mftYwLL6bJsbWfgvagnFRnUC&#10;aK8cd0d8cfpTPvj4JZ9/8Slff/kN33z1La+/ex0756boi3nAtPtsAIrUVzan4YowzcGJX1o0bUfD&#10;NtC+kKcNNB5iLx5b238rNuJNeupNenr/zNKQm875wmiVXUZjiudr+o7ZuPGWwh6NyO+2byWszDnX&#10;LVHzfsMffel3gTlvOQ5ZkEsq8V/7+EE+WebJ1liIib6FOeqepEnTsvdqePWoTTFWbIjy1RF5LzNK&#10;l4mMak9+vN2PfaCyX5mVVnQ02ji/h0Gkeq46ylEIpeY9CUIgFaen9eYOuU359LjFs7zV82j9dKD9&#10;+7SrpvLcv9f+m9q0UGw+Kzb9zLsdusGife8mDP4ubhOfVivzPi4wB3kaIlGWOVIeoUjFa2WN8ez4&#10;hI+fP+fs6AipFccgd611IVkLcqsTwSsRbGkWaZLVub264ubqmtevXnN9dc2w3WLV8MFix8wWmJnK&#10;3o0JONTcwyZYj7pTGCtYsMgKaWmm7lH+P4BrujZsBhMPmYvDDMWugts9f7CbD4DJ3fgnfyCPpt+T&#10;kWjAewLbbZMvZwIKrQx4sInSHIPTVtbmnoGcWT9DBbPILqnqdGT1V1PCjRLbNcT+SjJVNr0fopje&#10;9n7Dzd0tT8+fcvr0Kf2qJwr0JSDXYD4sx8JT3uwYTP5OhuxO/039OYHhkGFTAGYGCztz7RUBrEbR&#10;vqOnR3z+i884PTvlw48+5NdffsNvv/0tt7e3IRsTULkZXR81nochsooirV5mt5osxnQ2SXbAxT7b&#10;kUP56Pu98/u/4/H3ZFAfgI83XLncM+TBtTJ/3thxyR4XyZiLyS/3Lh2UNNuPcSw7+O8BLH7ohJjQ&#10;XkifWCBLyJsWYuwKGZH14ebONNJxDIAxVKLuedTgL5RU2ikQJ+t1z+1xoGt2rNyFcIy/dbJ44/u5&#10;tbiHcFHmfq8IenREd3yEuOV260yxHiKxwONu+V7OLLRyWljTjT4DCFmAjXbsq//lq01ZNQ9f90Cx&#10;sv2rdC6u+W/yeNwSmYpYiWTlSqFX6NyxceCo6/n47JQPnp3yZN3TUykSVcExJmZApIRVaFCkoxPF&#10;tgM3l5dcX15xfRmlqG9ubxm2UW1W2uDWWfmbzcreGmhIcCEP4ihser4twEXbTCyUcgTztSGdwccu&#10;UJgBxuJvllg3JqDnQtpf5VFGfOdMCMrJlZB9vZRPpAXXnmmG7bjfhLYXSuBfD4VIZNnEzrXh1pt2&#10;+C2ZuupZQIsEDKl8tW2fILlpgCvFHM0JrlUpPVPp8c3tlmEYuL675fLqimfn55ydPeX0yVP6dR/r&#10;vEZquwuRGj6JsyXY4gFz8a6ysgELWSxUQaZYL0FjqwEl3JySAZzqHJ2t+eT0Y87Pz/nkk0/4+e9/&#10;ztdffc0f//hHbq5vcGv7stTp9ybj6hi+tFJKMCQHwGMDTixeb5eZWqy+fTcLu9/bP3zBFvsbrnuX&#10;Y9+0fPCsds0b/Rxx1fKSpvx3Wrn3MJkmhE0AsDEcSyC2z3Qsf7b11y2Bxbsp9rdf8zY656A1wN4k&#10;+BseP8b9G0WbzFBYIll8K+oLhJVnQ1iVNkZKKdWmgLC2xXqLKQBo2OVdjzf6CdOyaTEYIhKLOn29&#10;RcpkUZnC6vSE7vgIJDbFakCnpLBtE1pSmFnufBj2iE+UpCKYZwp7/qwJZpofem7jD+n1+TgMLObP&#10;fuh931WAHmLb/lZHzJEDgCqMWERivxWpRm/GkyKcH604KcJaha40hdVYhqZsnU46Oi2M28rV5RVX&#10;r15z9f1rbq+v2W4G8GQ4iNTI0XJzMZGou9JAgkV9ArO5TsUUS7SX7eFtXxBmlqMuAUcDEAniJ/bC&#10;ZwW47+qYgUUIU0+AP/XXgTE9OMzNaPKFWyQVRgPXunffib0kn6k6AQwaUGjA3MN6d5nl3cRsuu/U&#10;wWh1atw1QExLFXbHJXZURkCsYnR0HZlR4vgI29t7tsOWu+s7rp9c8fLlBzx7cU6/jqySiNwBN2Ha&#10;3vqxObhn/L31uibJluCEZDJS/6rLlCEkIkiRZLoiEPv07ISTkxNefvSST376E/6f//v/5Vf/5Z+4&#10;eH1JHWpUO+27qOtTK+mVCrDBodiLh/PgUBzM/nUHXQ6P9IPEhwef9djxvsb08lnLdj58boMhD7/v&#10;+csO+Ghgoq2h6aL588f6R5DFHIj36pY1Fw6BjIO/7y3wH9JBh679e1JEh463LiRfLKKmdBub4VA3&#10;2wlctJgK8axPITrFFiCNCdHpRpKUf+MIcqim5+4ec0pcHHqw7XuGSAiuPFOJnTC8FFZPjpGum7Za&#10;97y3pQDX/J4nQPEI2piUheXeD8Vya3ufM2OakhM8Y0oAsQcukGBW5gqVO+enPniz++N9AcuhWIr9&#10;zw9de+jz9z0idmQBvqbfdHYGeQA8IXa/7Q2OVTkRpc8y3qV4fj7GDsOqdKVQq0MVxqFyc33D1esL&#10;rr57zd3VNeN2iLiZ6hO4EKIkuVtkDVm1SZnXBQtRE1zgHu4AbzEVNjMde+DCpvLfizoW3t5/4QZh&#10;BhbT+GQ/LBmLfar7TWP5pqO1cX8063I8EhTIQnZZc4O0z5kxiwiglnIjowFqq4ER+4Z4md8hXBcW&#10;VWk969lowVXpWjyDR6BkdYsN/DRkiyIwCOO45eL+NdvbLTfX1zx7fs7Z+TPWx0cYxrYOsXWAgpR0&#10;hWYU9mOK9W39GTvQ2lxnhhaPBc3lp8nSmBmoT4xo2722Uil94ahb8XH3Iccn/yuffvYxX335Fb//&#10;9g9cXlwxjlvEYp+giFvxzLar4Yh5IzCYLfoH7pFH9NMP6Ye3yYH30ZvvcsztAtjT4yyZBQeZsz7a&#10;Z3N8ILsWTbv3NNcfEgJLliP0oQe4WAZztp0I3/gSb/jsfVmB/xYAxiEg9aYJVGZOEa81LLexYpsM&#10;PrIo6Tv5q7wtMmgwPqx+FlU1Dy2MN0/4Fhchvgj05OGABw2Zi8zaHhEBarTrWZ8+wQqxc2q+2rRN&#10;88Ic8QaERJBWLdNzB9osiqSeAbD5+iINPLWAxHj0/q6bnor1sd0449CDrpYHIOo9jh9EAf+N5muG&#10;2O6mEEOWPA+XlmjsNouF6+Oo61iXgowVFZ9YDSQYjCIFHyrD3Ya72w1Xr694/f0F99e32DDG5nUW&#10;5eURjQymVkbbE0xkUTdYgAvPOIoFQyFZNGt5rl2/ZDKmz5gBqvtcZG7XDbIHLKbzD9foIYbj0PEo&#10;ODygeKbf5y+TK2FiJ3bYjOz7+LXdJxiGiQnxLA0+PTdcOSog6V+sJWpfuIaTpKBR/ZMEmMn4qHtU&#10;xfVCqULXCVJiHO+Gmyx4dsvN1TXnL85Znx7TrTr6rsPEqV5zI7S//nBpqeDNKJL5d5gGMDbYg7aP&#10;SGNFhag+KyL0Jz0v1i949uIZn372Kd98/Ru+/tXX/Omf/8z1xRVWK30psS4sq6Imc7oc4wdud9kd&#10;3/dhzf8apn1fOe8c79GG3XPwQGccIDF2WIv9dh1+2OK6vetF5uDzvK7r+x6Ya9KXUqbFvX9MnXjg&#10;3X+MQfl7AIz3fd4kHM0DUKTgleroqFPmR+CICH5sgnQSUYu9OSa1vRjAqY0t0W3fj7nsPycVbpbq&#10;3q/s1S5LUGAa17gBJUp6r09PMJXcNpxpBmWcPO5t4mVgJ/PfiOMauyPspAomuhBZABtvu4YeYB4E&#10;xPVBy+c3n1mLfdajuRDeDEz++uNN8/THcum1TJg59S7GwUWpBLXsJHjQQt919FL2yp5n6um2cnt7&#10;w+3VHdeXN7x69Zrt/YANEQvh1aO+gNV0aQVtXtueNjazDxCpxC11tAVnLsEDblPq6bzh2DwnqtUd&#10;F0irOtC6roGO+S32AcZsobn7HNorksHGTOvtseOwu2Rely4ttdIel1fhv5m4iLZ/SwMPk9EAYbFP&#10;gjhXeyrSCEb0ye3UpS5WVfDItKgadWjMjbFK1iLpJldAbEqYfY/AOEZpfpUosLXZcrHdcn11yeXF&#10;BWfPn/Hs5TlPzp6inU4GAW3H28Wr/hDADT6xFy4Zb5IgYnrXmmPYZF0b2zB5adtSm1kEHncRW3H+&#10;4hn/y9n/zH/3y1/y7Te/41f/+Ct+/7s/cH+3BcItEkljD2Xocs3KNHay8/n+u+67Th47fijIeAB0&#10;Hl7wTgDjXXVXU/iPf/4GoPPgPgujNq9vdTFmoJEFnAFubm64vb2l73tOT0/puqittbPAf+BCfWMj&#10;9ybr+yLHR++/1LkH+usHu3EWAjvmfwS11bHOmR8W9QjUgkaWFDQuMIePyyQMmyXf7troyKhOd6gl&#10;B6Dnso0TGZCMyIRslupYklAV8Ah8qgrWdch6RTlaZbBmxE9Eyqpkam3ef+qKWZB6Mi5ICEw0LdTq&#10;mI9YTS+vz+FEbzpaqlx7gfYqbxu1x4qa/RjHvybojTgZZ9+9I0ikOPoMR4sofSn0ovQirFDWFEps&#10;J8K43XJ7ec2r//odV6+v2NxtYyOw6pEpkvPTLOa1ERu7jZYukGk9BiioS9Bge26OPXAxuUV8Fxw0&#10;f3vceo+V2GMp2rklOIkUxKWx4ztxNg3Qtu9O/bfHTB4EGAmCp7/z/oeOfddJqvVM4zuk0JiqlqrO&#10;NS58sY5i6gtdJmNJ1qSY3FTpEgtxYXjG3rhqujGdnhIpn8OYACVdmeLYAFevLri/veX25oZnL1/w&#10;7PkZxyfHiGqMO8GM+bLPkg5/N2rQUyYse1GmX1Waiy/AbfRHyYymVp2XKPZGbESnKqyOenyEF/2K&#10;p0+f8tHHH/LlP33Fl7/6mu/+8h3jMMJE7+8CJGW3Pe5zam5cvff3e6z3CTQs3vpN1z76nB/JQHnn&#10;Y88ObUBh13idwdhDd0tz98U4a/7rAC4vL/m//vN/pl+tePnyJR+8eMmLly85Wq9xoiZDq/muuqC1&#10;Fw9qAif8hfpAOCyPfctkv4MfUxKzsH081bAB34agJ3XWFD2zEFji20lBS6NlIz5CkoEIZsDBoA6R&#10;9VHHMWhknC4j7XSa2E7jHELv+vRu0mxRn6fh7OG2mSGYlGxb4LbLIEz9tuujn9SBEBRl9ouY0zkU&#10;FbbV8F4ZcO46eHr+FEqJwjcJnozMS49KWdNRtJBRglhbxNqEstJJZMpUHDNl64ZbhazHYAleJqiT&#10;VldUi5OM+VjUCFmmqMrMqk0MxcQA7WbHxLA/tELedhyyWN4X9L6r9bM8PNmb6fWI/qoOhUgZxR0Z&#10;K2sVTvuelTtrUU67FUdeGC7uub644Prqituraza394zbLTamYM0qmJPV2P6ZMZpRHfKi3DzMYkOq&#10;HCvPWIm6kw2yex8znzaf2wENU6yEPNjZNMmAWBEtG2Vhi8qUSuVTdtWyn0KBW1rBcSdr9836LgE+&#10;DoyFNAZln6HYG89J0M5PzlGKtueusS3Kvl1m6pPlZ5bFtyaZSTIe8T7VhDnNb+J2omhdWvbuUR1V&#10;RWINtD111KgSbltFqVVRLxE4mazBcL/l8i+v2N5suL+45uz5OU/Poj5GLVDFEjRauoPSrSOZ+ZJz&#10;p42X7LBPS/ksi/7Jdc3M+rQjI3liq4N0J81YJmRLJeSQitJpz0effciT8yd89PlHfPXll/zu2295&#10;/eoK24JYT8nif1Yr7gHEZ/aYSQ4h4Fl7pTV5YjZ2BOoMGJerk8XcnUsCPozZeTjXmFi5nY/aPHvw&#10;lSVs8am/Hd8hqlvvT/py71Yzz7YYuz1ALTAxba25rbKsLPSVCFNdi8Zox/QUyr//D//h/7i9veUv&#10;333H7c0NFxcXfPfdd3z33XfUWnl2dhYUnOrkOmkdXEp5YGU81inLf8vrDiG4plNbaVLx/f55fNjm&#10;tRyLc6lop2uSjpw6c1ooMWDtU81sDqmxE2ndVmybJWrHSCcNvRs7HapHONWkkN5Zn/neT/b6ZNFH&#10;cujGe30oGWUhHnuCSIt2h+KFTkpYrl3HRhw7Oebsk49YP3uWVTDb5A32IlLqFmKiLYypebHVOhqT&#10;q+CUBKKNlg0BZXNFwljBqMd+IsIcw+KyP9qtP20WDMveWy46OSy5aqsAACAASURBVLyo35d5eOx7&#10;73K/x4DF278bbFEM97wAWnqfu9MJ9A4nXceTvnAMrB2ORfCbG66//56L715z/fqKzXVsKOYZcNvK&#10;qE/sw+Lf6BbukAy+rD4HazYg0bI8pnMtpXS6T9aF8zn9Nf7JgsWI5bYEF3Nfxf/23bOHDJb5J9OG&#10;Zy0AuX2+jJmAQ1U/38ycPHjeA5k1v5dAm4aLL0+X7Y7ywXkwj7FP99398tS+7ORlXZCpnfmVVEGL&#10;r0vreKw643bg/vae+9sN4zYy2kpX6PqOoqlCJ0WY8/Cx+esNZCi4BkBexFssL/QWgNYU+U6f7P/b&#10;655Gxxfh+GTN8w+e89EnH3H+4pzVqud+s2Vzt8UTPGgavKVoFA9MsCzCVJm2AdhSCrH+5nFuTUhY&#10;PWGONu6Trm8dL0wZcW9a68uvPXjFR781N2v6XebxnnrNl9c81J6axbAe07+614IJWIg8GJ2WctD6&#10;UwmZ3gE8f/6c//gf/yNXV1d8/933vH71ilevXvH06dMdFqJN3lLKFPjZAryWsRpv9d0sAMry+h1/&#10;12KBvj1LYj6ytMTO3/vXBshbWDwN8WVxoIaasRq7kQ5jpDxlyeMQIGGBNcUY7oi2uc7hd57a9IOt&#10;4BbZ+24KUhPp0yyA9u1kJXAyxz3S0lb9iifHJxz3K268MnZhBU1psgviIsatBRfmmMlc+Epzhldt&#10;VQs1qkRWwUfHhyiiE5R7dqNm1LyHBaO+GzexzIaBpZeq7WOyGHHXv6l75F2ONwqUxbx/wx3m6+Xh&#10;qDfQ3ZVUAAnQMOfm5obt3TUy3DMOw7SrblN47kwBdEsla+ZT7YnR6qQsl8GXu9c//HtSbum22AUW&#10;PoOb9jcpsPeVeQMh7LlPm5UYf2R/ts+y33yRdjtJ3nDJZaZj3md/zKAxgs1q3V2mkoTF/jqcrxPJ&#10;FGz3qZBeC9QO1rMZV+l2tHk/kvZ9kbY3SWZYiUylxB3ZSW/1TAnXaRuAzCzJEvGmURND7f9j7s26&#10;JLmNNNHPAHhEZmRG7rWRRYnqlihKPXPvw32/58w/05+cudNaWy21NrKW3GJxd8Dug5kBcA+PyCyS&#10;3We8TlRk+ALHavaZwZZxmHHAOQ8fHCJHdDGi7Tq02y0eHu5xvrnA2fU5FqcLzEJAYs3XI0ObGbwI&#10;DNV40pCmEo86Wccpz2kTHmvaiBEQ3ykBSCmC4OCDV80PcHl5gdPTE7x6/Ro317/Hb/7X7/D3v/4d&#10;200L7wOQgC5KakVyhOBs61/ph7neR+373FCrF5dvRsUkym1FoTXtrXKI7u9uNex/psK3er+6Se/M&#10;6V1QQTvXJ/jwxNo4dBRthvWdBvf75b/8y6+89zg9PcXl5SUuLy9xdXmJ5XKJ6+trnJ2doeu6wSJY&#10;r9fo+x7Hx8cZoZl6L4QwaRB60KBk0LE00FQcbNeei7ZfCNJ9veodRKT2EcjqrQoeSMf24uIXtx1S&#10;1+VtEGOylk/DlEvmdpkJHoZhzL/r/t3ub8PLblT++HembKpNEGplBmOCBggIHpEIs5MTnN9cYbZY&#10;IAWHPjj0vgATH1ndSbWX8hYICgghgtc+JcfqbSID6T2Bgocjp372UK4wAmNUmYjycDHUSLl6oExq&#10;+9B4J3y6Pz/lOKyifN79n1YXW+RFHBKiXOawJ8LcecyJMGPGDIBnRlqvkTYrpK5VdbAgCmZG6lMW&#10;Ciwstxhnsuy1p4Q+RoljUWkzYozZiLP+TIEMZtbUIFPRNVEZhuo9Iw2E/F1J5/ncWFPBhbGNei/t&#10;0ONPG3srcZ9dxqFys6YABmTK+eFd+teEliSXUiGnWpu4W9IuY7H7DXhPaWlMwrYIzSlGtNst1usV&#10;urYF9+Jq68lpzJTiOSP0tQbBheZk49apdTPqNhp97NohUcrRUPMMFi1p0zQ4PjrG9fU1Xry8wdFi&#10;jsg9Nps1YuzEXVeTMcrcKUIOkQM5dfEl3RKv/jnVBu/TwhdgqMiwAk3j+wb9MXWuavt+zak1fXdO&#10;0J7nqLp2iD/tvr8SUivNhRtpMgRUiNDqiOD/5b/9y69q99P5fI7TkxMsl0ucnJwMOsZ7j7Zt8cc/&#10;/hF//OMfcXt7i5QSjo+PsxGobZ0cOqYZZ9UyViY+auhuQULGhh+orQEUXOSX5HIclNGZJJAAjgBH&#10;ARXctuC2B3dR8yhIQBwPp88VYCJqNSNG0xWeGsjxJJ2atLtl2AizLmIDFdL2PK8rWw52w/fJ314y&#10;moYGTAQ3m2O2OILzHhQ8+iB2D2KcWoaFnAOcQ3IwiFq2kEDwSUP8OkYklhwK2duV4B2hCR6O1Cff&#10;qUUMV0ScJH6Ah/moAFD1av3PfOJzOmuCbJvQ9Iz5PuBi3H9Plfd9wQVX4ywCOA/OOxCCI8xdwAyE&#10;hhNCSuC2Q79+BG834hodNZ9HYsSoH2X49RZH4gpEmK1EzhNithUWjwIl5XsafXPZkqi1GTK+ZjDH&#10;2a25bIlIu7ImC2Ptxp5zI4BRn6/Ha2qLY5LZ2nXaHaNxmVPjmb+B0k6rAyq6hAK+s2YV5pIOjLf2&#10;ClQp2pySOTbt9pG+C9qf8l2gjoGmGJPYjLGuNRaA0Xct2vUWm8cVtusNCIRZaBC8LxUiKgBSbSHI&#10;tmDBZR+e6lZwnsVZmMt/u1yHwrAMZI9ACPkMuLwBBgvMR8DR0Rznl2d48eIG1y+ucXQ8Rxd7tF2L&#10;PkaVragCRwRyXkOjSxsdWwRj7Xmtg6vqZABiQLeNz1jkt2oE6/lSbzHszCcg85TJZ+gJLUjVd/Xh&#10;duZvPT5lTHIZeV6XOth9jsp4OK2TgQv77X/xy1/8qk4YRiSGVKaRYGY0TZMJhYGLf/zjH9k+48OH&#10;D1itVlgsFpjP50+q/Q+BC9u3HHfNDran4VlTtcm2CO08k93AWLP1gXS/UnMm9GpPoaCCY4RLxfo7&#10;Y2XmvGUzlmzIJIDqzd+XqQ3LoMnf4/fYtgg5vcMsv1WLkReQd4D36kMvkRfhCX42A5FkQDU0nmQW&#10;IVbAwhaHh+Qx8eoNEkm2PoL3MtFY1HZOywrkEEhyJoDI8pBlQm9tcQowxPbFQFMNMBRYKNhIzlSz&#10;4x3DH2Yc6nKeU94hUPnUcwU0AsV1uVhiN+Sy5iL0PeJmg/b+Ee3qAa7rgCjGxgIaDGSotsFcSStt&#10;RK/AQgBGBRpSSTjGNYDgKc3E8JPjVVRhwrN6eazR0HWU7Fr9rf1Stl12A2U95xg/sxfMj4hpff8z&#10;3zQg2mOdg20FTV03OjZgASycrfAqY1y7W2x1X2XxvGJytZ5nPFYM1U4mgPuIbrvFZrNF27YSDJCB&#10;4Bv4EEQQUePYpGMCIlgG36Gd3HiMxi1UUFFfoXwF44NVNWU8qut7EWqdU7faHgkRR4sj3Nzc4ObF&#10;NU5OFkjg3J4UI2TLNqg2gws/H2uJeKQdHTcFo3nIo8t7gMhBAaX+e/K+54OLGshM9OYAFBfdcU2H&#10;aDCfDTMXzQWpLbFpLqRc/9Of/exXWS1mlstUfgPIVs0GNBaLBWazGQBgs9ng/v4ebdvi+vo6nx80&#10;avSpF/kA1NgixpQ+QruCAFBSg5vcI8UThGQJEbiE1yVVsydhgpRItj46Cc+dWk0i1kcgJrjIwjDh&#10;cge6wULVdkwNLJV4Dj80Q6vBhSFY+e0G99qeLYBsQW7Ax0HABRGBQgBUHRg5oe06pJgwYw/qkxjl&#10;OAf2DtEBnWP0YE1WITYqnhw8u9zXpGMRiOQaObhEuq3i5TeK6syMhU3DwUhgBQmmhTAr/KGEo/1B&#10;BJHwOSdjc/yfBy6eU954XtfPPKseeTxNY8FK+KVdgRyCkxDtPiZQ12Jzd4fV7S1426IhFnDB4sbH&#10;ldYiccr5JfpUck30qqWI1TZHHIGQsb3FeIukfMwLxLQctnSGwCKBsuHntBai1lLU4AKZag/uQabj&#10;g7EYb68cGgehQ6NCMAQY9fjW1/K72ORzaXjeRsjf1XVrg7ZJmlXmvbTdDAft/kyG8j1W4WyXkvu4&#10;xha1gSjEm0RpY6UyqtYXZKukbbFarbHdtvp2WeekBtuo3g8gS/XsRtobo9Pq2UJGM0bP5344sFRY&#10;aQ2R0DbfSMyPPvZgJIRZI/E9UoQPATcvbvDq1SssFgv0KaKLHfo+IqaYGb+BPjFeLCMw1kyUSmZk&#10;kccxa2aqOWYMeKo9Q8ZefZ4AIFK/8bZ++WdlO7L0EruAhTDcIQChenqXz1TDU/oeqICFjSnDEYvm&#10;YrylwWr8Yy+w80SyNXJ8fIzLy0vc3NxguVwihICjoyN89tlnaJpmZ/FNgYuBxD9ZewUZ1XcGESOL&#10;k2EUSVFpSccJMbbU4I4JLkFBRY/U9gVUcCrpjW3QTEORBUeZUCY9CZCQDjb31gTGzmhUx7hvnnPs&#10;11zYq4Z97RhZRZnBxWBxkHpxeIAA1wSACF3fA31C97ACb3ux+vUe8A7Jm6ageo9uE5kGwnRlDiWw&#10;Clj7nyTYkwEbAxahCZjNGrgcOYjRc8pbHdVqQ7VRootVtqusv2XM6+U11Yc/zPEpmotPfvcIXNi3&#10;U1fbQISGCb5PcH0EtlusP95i8/CIGQBi0bxxKqrvlFLRWKi3RzJtBe+ChN28ILuain2aiwwm6vMT&#10;GovBfVVrU3W/HfVzY43CWHtB2KUx42Pf1kgGAp94TNVPRq6ApnLO6MCu8DRCRqP2Hagz15L3uE8Y&#10;U41iC5A2Al/I42IAQpj2er3G6nGF7bZVV1ova5oqIczW3oBG10zZ+EHtPj9B/8d9UV+2tPamMan6&#10;wOzMImtQQ+8QmgZNmOF4scD1yxd48+YNlssl+r7DerOSaKCWp0Cji5pqv9SVMpApWgAq9TTvEoII&#10;TqCd9V+T6p05+wSYqJ8pjH1aeD+sqcDgej0xxgDqueCCqr4qAbUY/utffP2relGYaqPulKnKee8R&#10;QsDp6Smur68HWgt7LqWE+/t7/Ou//ivW6zWcc5jP57CooPV9A4lCmZhzkirYaaY7cg5930PUcQRN&#10;H5gbJoWxSNRw6uetKreec5rzftOC+yhpi9VYkSz4FautACDMElSYHBRMOMqhtDlvzFWfA8e4T/dN&#10;DNMmDT9uekyIrSsAAjxkP4StDTQMiGOaKZAADDILcpKMor5L4E0rcRE4wXsHHxwceThn2V6TakEE&#10;q8fESA5wwWX3LsPG5KxyTqzotf+QtRUM8gTfBDTzIHE1SNN3G1MyQqyIxWwubF07MEICEIvHSt2/&#10;9fGp23bPfc6O8diNy6i1dFOHSfLWP9lolSWGyMwFoO8xh0OIEY8f3uHhwwf4mBDAcBp+22JPmEsp&#10;V7YU2RajPm/2Fr25mZZzljcmJWiMC9v3l3HJGgpl7HUsm7LlwXovBsBi0HbVliYq94rqHVnCr+0U&#10;8pxQwm7Sp0n29qk1G9kupPrYGrHaTI3dQBI9sI7rNrNWZHyebLFWtkTVLMk6jYRK4teGWFQBE7YG&#10;ACWDjoqZo9KCwPK42OatwoGBWx1VdQBS5QIdezGQ3GzWiF0vQoMX93bHKpBZbVSzIWWoJ6GXSc1s&#10;msnqlVrbAc+2vhr0b3kg92t+wH4rvdRFxBC6M5vNcHZ+hqvLS5ycngAEbLZrtNs1mHuQg7rEi5aP&#10;vCveOlyJN8qjck8LeTO9ThGCxoy6EKa9NOKp8wB2PHEGAieKRmLf8+ChXc+UIDQFLkj5X21zUYML&#10;wRCKEb7+xde/knflZbVnIQwPu2ZWugYYbBCcxsX4+PEjfvOb3+D9+/d49+4dPn78mL1Pjo+PwVyM&#10;cuw5OA0xrL72BipyDhQgqxmtM2WvT1TujSJHp+nNY9sLo1TvD5fUWDFR9gBx0NTHeY0NER3nCe4G&#10;BqOlAuVzGF4Mj6m+3Y9e94O98X2kxNLAxQBlZhTuFbEb0pYgYA2TBLZKCbFr0W9bpC6icQHHzUwi&#10;kbJMMnIEOOmTZJwegKALlwEXO9l+Ia/3O2E15AguELyXZE3BAyfBI8QExB6k0flkd5eE6diWCcl8&#10;cxA7Ds2JmBf+dz2+r5ZjvH4+uXwbLw2sBGb1HXfwTPAJCAzMmNCvVrh/9w7d6hEBhEAs/uUDN1Bl&#10;DEnCeA+8QAYxKjhnPN3RPNTncFjyt2MsWceqHFTl1/czoInxhtsk42N8rdYQ7Lquj57dQ9ee0ogM&#10;6nmANo7LK9/DZ2qaa3jA6j/17p03KdC2Mms7sHH9mA2u6Dtg5MrQSdVmMgCjvVqBFUkC7xD7HuvH&#10;Fdr1GhwTAjk0TrY9iajYzmidvPcAle0ykMXQsJKVLU8KT7snBtohGt9jJ7L0Yk/BhNHZvMHF5Tle&#10;vXqJy6tzuEBYb9dYbx6FpniXyyzhFoL2r763GqJcHeZsfF6u7dLn5x57BXwcACF73lCABUqf0PDa&#10;c8DFAFjk+6DCrwEQLuAiFz5RqSkVztQxdV9KCW3bou97PGqQrru7O3jvcXl5OQiB2/c94Bz6GFVl&#10;L5OSiND2PYKXqBKcDFDQyItAJG90CegkNHdse7GpiAnUa9Ar1q2DIhzqxxYWkKg4OTIVjUVeiwfA&#10;hKsGfeqzrx+nJsqoh3c+RPV5GWDWrYIc68Kp2rKydRBpwKmRpxhbm6U2qKj+uI/AukVab0FtBPU9&#10;GucRHMH5gEgM5oRIon0AqTklia2GAAsgOYA9AZ5AChC8I4SZg4TVYHjHOAbjkh0+O13i8+srXJ2c&#10;wKeEtl2jjx2cF4ABL2moTSqHjo9IQ3wQXBwam+cAi6eef0od/9Q7XO7/PAM16p2TuCp9wpwcXBdx&#10;/+49Hj9+ALoWjXMIBHhlTkbIx9qJYmcRdwJiRTZth36j9gTRDKhVdlPLG1I8Rcq/WusgniZiLG6E&#10;zSQ/Y3q2nhiaeIoVIBuarz7Me84DmRYMVvZYlWGslSomVN07fb8x7/o6chlFSzGcK+Xbyt8FVfJt&#10;fD53BGoYMNBm5A9lmgh91rQ3VvYOIARMOiuanmoMRWs0ek+pnKY5lzDdfdtj/bDC6uER3Ec0ISCE&#10;IHYQ5EtsFQgdstGpD9NIyVAXgTHT2Z2jjJ2A72qE9L+yQUP18ClkEs1oMws4PTvBi5fXePnqBc7O&#10;ThEaL54lbQcAorFV/lTWrv2tzHkw3sh0dppRVwz+mRijaD1QPGfqsjHiOfvKGfX8/nk6/B7fp3Ld&#10;0BWVSj3N22kALmqVyfc5bDCcczg+Ps7bJqenp2iaBn3f4/z8HDc3N/mZGKO4sxLgGzHG2W63AIAQ&#10;SrS4btsWN1Lzp4WADEQG+oh+swW3vcSn6IWw+oRsYGgRpK3PSqeU7/rveoKzSsb1MRgw3g++8j1T&#10;aPOZzxwCKgWFQlSSynBdRuwSfY50jIwhu0FbCUkVDo4d5uQQEgNtRNpssHlYZ7VXaGay/6l1ME8G&#10;MlBB0O0Pe790vPcEooiZBxowXOrRpB6LxuNmfoQvF0t8fnqKN1dXeHFxhrOTIwRPSLFD262RUp9d&#10;NCVnAmXCjFF4+h/ymEL336eMfWCm7EerdpCUXDLBM6EBITDQ3j3i7ttv0T7cw4MFXEAyZHKdxrza&#10;/qg1FznV+U6cCmM2o/ODvy2jKT/7ngwKSkNzK2tJO1WaDVTX6mNKkKnLoIl7ntI2HNI4lWeGQbPs&#10;3O4xbdRbrk23o566Ra8xvHf/MzX95p1nBoBmBECyJglQjzsUY9BBefKfwCkCUkLftthut2g3W2zX&#10;W0QVDmfNDN6H7HVk7G2gdeAq8BZV66Nu+OjYjfMzZLSmoSidUwCLSZQSqFMAkvcOy+Uprm4u8fLF&#10;C5ycniClhPV2g227BRHBB5/ztGQwUxnK53MKLmrgOb3GhwDpeQLOriYBKPZt+bNvfqMAjL39V/V/&#10;mb/Dehu4oPq9Fbgwg1j/9S++/tVwcOgguJhCY08ds9kMR0dHODs7y4G6Li4ucHx8nNWzAOCDx1//&#10;9je8//ge3jvM50eYzWbis9/1CD6g8Y1qHvSTCBQZqUvZUJO7JEBDV0gdAGqcSXOn9zAyEK2BBQ3v&#10;zSiyRs7YP7j7jkP3H+rjvSDDeRXGqr1/WPCZ6jk11jKgARJGTV7SO1MSFXtg2Yf0ypy26w0Sx+zl&#10;0XhJnuVJoufJFFLSRJCwu04jdXLCjIDAESFGzFKPBYDzecCr5Qk+Xy7x5ekZzp3DESUsmoCL02Oc&#10;HR9j5qUesdsixg4x9nDewWna6B6kVujTIcCfexzSLnxXzcahcsbXZXOvluLUjZcdAgMNOcTVGvfv&#10;PmB1ewvX95g5QuNIwq4bswAyuBh+F+1EtpuopeYcUhoZkJTfqCJtym/TnGdtw4iRCUM2IKHgo2Ku&#10;zJbQS1dRxcDsu/5MnasJ3fg4pEl6au2Nfz9vrT7nfZ/uUVRK3QUrBgQyc9X7iLLPw9BWpdJgZGCu&#10;HZgDDypfLhYbAFi2IaHziJjgnZetki7icbXCerNB7HuhtySxI8Qt3WcNi+RtGo5YLVkf3tbc1X6M&#10;11pmovZRbSmI4SrPNBOqEjOC91ien+Hm5gZnlxcITYO2bbFeb5BSlG15cnkgiIrhp42A/K7j8uQK&#10;Duv3xDjzFP2g4cgLwHv+MZwbw77L16maPRN1nAIXrv5NZrdJ4i1yCD0PC91PEAePV8S5dmUzg07L&#10;vGrXzO5ivV7jf/7P/4W//uU/8O37b7FarXPisKNmBkceiLrfHxncR8S2QzQtRRuBPknaaeaMknM0&#10;zol21YaZ2TjKEH3d8ToJ82+bYPrtBqq557O2fYznOaBi6n4xfFVX00o1Z0gZKAaHIMqxL2zG5DIY&#10;8JBFaXYp3nuExqPvesS+R7ttkdpO4i54n0NRk3c5UI0jdUlVidpzjyZFHFHCqQ+4OT7G6+Up3ixP&#10;8erkBFezGY4h1z0xHCV4jph5wtnRHNfnSxwFD8dAbNscBIgBSdbkPIjTxBL69OP7aCimyhoTl33v&#10;svw0poGpNReBAdcnrD58xP37D0ibDWYEARao7A1MErVPqr/VnmmUk8J+2/e4jPq3CYS1LQag35XR&#10;Q3nOmNiufQaPOMlTNg/1fVP9+dRzzz2eAzCn38WDuo2fm0pbPfW+6ffT4HtHOwMTEHUtW8fbVSr3&#10;aU2H5N40TfXtnPI4EpA9TMxLRLa0o+bycIh9xHazxXa9Qde2ICLMZnMNtDhqY5akq1ZNgPPh+qGC&#10;pPb0UNEdMEqyQ5mbMgfF/giEHLnTqbFpmM1wfn6OFy9vcHV1haZp0HU9urZF7PosqA0qoOvC3DGR&#10;43bI/wOoXY+53TXFB6o3EBWtj9w6HQ+qfsdUv43v3+EjE2XsljsC9NXvQvMB//UvfvGrnQIwVbHd&#10;Cj0llUmyGJ/PDQiVHnaPcw5t2+LDRwnI9XD/gPvbO7z79h1WjyucLc8wD43knTBXUo1TwV0C1J6C&#10;RFwr7YAmxrK22L68fmoX1xJbASiqt8J0CxJWhIbiZzy2szi0QKYG9lB/7uvfyTLUQJNpVE+iqrYF&#10;WMifprmQhedY8pV6pwa02j9J6xecE21S2yFuW6S+l6BjUCkmqJspSTniCsyYIeHIOZz4gOvFMT47&#10;W+LN8hRvTk7wYn6EM3II3INdjxgSEkUAPRwi5kQ4doSTJuBqscRyvsCMHFLPEgAtcZaUstT7PY8f&#10;ClwcUrOP32HbiUViLMTSMcFFoH1c4fbbd9je3iOkhBkkUZxXjkAo2xNQQFF/l0BYmhod5VzUc7Bz&#10;KODBJFoDCPlaPq+VHanl7SdrmbvMObMBiaj7DHBRX3uK2R+iZfuO54CV/ds1w/O779xlC4P3odgO&#10;TdU3l76nn0iv5W0Ovad4jBig4PLPQKKVm0qwwDww2rah4Gj2N6R0RO6LUQDGZrUWppyNIkdbGijM&#10;ybzMMgAq5Hei/4bP76RxUCBRXlVAr62x0MzATBK1k0k0K1oF7z0WJwvcXN/g5YsXOFuewzsnAlUv&#10;uU1qOkP2CuuHMWDYIxDmZ6k6Zx+M2l71BVX350+BKlX8Eb3fhN/BM8MeneIrzwUXgzgX0NwiP/9a&#10;tkWI1P3RKvfEInwKWFhMewvAZQDCDrNKhiMNNyzZ+F7cvMDlxSUW82M4eDzc3oMY+NHbLxCcx/ph&#10;Be4VVERF0HlqSTc5N+z0bNqTUWU1auNvUBWP04CIqQSrtnPRVoxVUyb9P3U8T1LZ/8zkwANZp5j7&#10;JfdFpcnI/VQ8OmzyBudVWlHLWTWuzbNRVaPBBXgipLZHv91K8DFH8E40FQ00NDX3mANYBo/zJuDV&#10;2RKfXZ7jzfkZzpuAoxgxSyxMkhJaF5F8kgUJhktAAMMnhovAzHmcL05xcXICD0K/bcFdB4akdq+1&#10;NN/n+K7g4jmS8yGA7shJ2nsYSFbAlyTPy/rDLR7ffQCvt5gTwacI4qRSZAJBvsEowKKqExcuUoAB&#10;6a9U5vre/frqd1avj9ovH0PuRStY6EMpw+amMcFPARd1P+67f9zX+2wxpu6f0qI8Va8x4a7LHP7e&#10;AzpsrfETs3ifW4x05O64oIY9PLgw3Cap7uRqPJVDMTR8gEV2pqp8FgNea13sI9arDVarFfquy96F&#10;3vkBSRlUpmKs0+3fTzdJaUbeGqUMcwsT15PJtDFKAxll7JhME+EkPsb1Fc4vzuG8x3qzwbbdgGE8&#10;jpRWFVs2VqRt/VO2zne1AxmZ6OcQD6bquR3aQfYcMpgZajz2z6YpcLGPv0+Biwx2QCAz6PzF11//&#10;KjNN7RDODva17DQaaqcDtJMJlVWNa4uy7L3nNLYVsxOvDOsZh8Y3WB4v8fLyBjcX17g8v8TV2SWO&#10;mznQRfRtD46CPLdti7vHR/jGwzVevBHyWAnyDj4UYmXBwVR95cmBU0LjPGIUVTprFVmzkZm/fRk8&#10;C5Gba229nruG3fQgZnTNxSBq0LtGTPYO6pBI7pOuLLy5CRIZ05pkW22L1AQoB8MRMTk/I8hUPU0A&#10;cEIhDgwEJoTE4HWLbr1RN98ex0Q4JmAZCC8XR/jRzRm+fHGJV6dznM88jiihQRKp22Sq7MJDxRuI&#10;fRFj4MCU4IhxFDxeLE9weTTHnBKAFtvNGuQDou3tgCuCXw4WHgAAIABJREFUr+mU2UIHF00aWWPc&#10;/oVdjQbGHgbDz9PgworJHhPGfCEGm3mxq8YhkIdPjPi4wof/+AvwuMIsJjQpIZAuR0cav4AVWEjB&#10;Fg1SNAepYgLF7TRlhsTgWOJQ5OtyWaVbJd/6O+b77LyGi2dCRBaExQQqS9Au103qVRhcZm0GRCY+&#10;qOav9fbgnh2pfeK+0fvsk0F6PVSj9bd3SDN4QmYwNufqOVJ+y1aCPGvGAEPaUoCIaX+lFTYWmWVV&#10;gCWpgMNKw5O9KwcZMSZrcSh0riijK/1Rg08bXxkv1HPXAMhgSw06D2X7u9tusbp/QL/t4AiYNY14&#10;lgBIHDNzF42A8JGqZ3XZKfO0bJFGOKs974qj6DcN+rBm8ENmXwCV0D3K9CjMA06WC7x48xIv39xg&#10;vphj022w2q7E883id9TlOgX7LPTSDnPXFcNHq+tgdmfCoKOePzIfUMW5sBhRCkwymBry5MzjiYbv&#10;Kb1bzW+7d3hfLm8CXOSZSGof5gD/9dfiLTKsTFkg04dIrmPVZEH6YklpiFGn8W5zNOiKARkJDe3h&#10;e+SQ0g15zEIjeSvU2MxBosN9+PgBv/+3P+D9xw/Ydi38rBENCRPmYYa+7+AgBM1RcWvNkx/QfTch&#10;cF7tBsZtMkJmah/L/ll7WQAKKg4RHh6VOzpcLc1QuW+flDt1zbYmysCTLtKCok1jQY4ymBGwRRq5&#10;0xCvEgrNqEQQrU4IXlTnKWEWAubegWMH1/VovMPtx3fwSLg6OcbnN5f48csbvL25wMvTY5zPA+aI&#10;mHFE4AjHSWmiMJ2UQ++SBc4AsQexLl4CfCNppV0fxeBzcYTl6QKL0zmo8bjbtOhUM5ZDnVMVCIdK&#10;6unc986BIV4UJYX1vuOp688DF0y7jIoAIKZCPHTuNhAjzsdv32Pz/ha+69AkhleNhQQmE2YgbtY2&#10;r4fM1DxBiiq8cjnM3iEGIioGPGb8VUszHbB+HryzYups4N3tPjvum2dof556JkvjNQgf1X3qmAIX&#10;n3Ic0i5OXadq/56UURfiS6DBfBwzgJoG0ahNw3PDfB9WtoFEzuUBRWtmAKN2S536f9TCisnqoQgm&#10;dj22mw3W6zVi1wFIGpBRAvSlJDDIvAPZpD0DTwrIMxin+iU0ylFS/uXcUjCmXdHt3d61OxXQyMc3&#10;Ds1RwPL8DK9ev8L1i2s4T3h4fMR6vYFzhCZUWpkRhOEceLAeTwUGuYmlnvtArMvnefA95N/TQHif&#10;NmQXOA/LHMxZGEusNBewuYjK5mIKXNje2uRH3psnKg+GQtRJ0AlBTqVkdXvMFrSi9q0ngYXmRp+Q&#10;2h7ddou+7UBJcimQ1ss7cSdt2xYfP3zA+/fvsdo+4v7xHncfbnF/d4/gHBaLY3SxV99qlbyI0PUR&#10;vgkZcTvvBTEHYULFuwSVKqv6cLW/NGLsY2+SPBlGi7oGDVNaCAMxzu7F6DfbnKfcf7aABDWSal4g&#10;2gvTBJiLqRqxVrhJtE1e3mIp6R0cvFp6B80B4hyQYodmFuAdwLED0MM5RnAJ5BJuXlzizesb/NPb&#10;V/ji9Utcn5/iZO4RKIFSDw/NesoWHdCAQ4kMqzkitUc0TbLmlOnUJoCiEGEiQnN0hPnyBO74BOsE&#10;9InQd52ACbACqpoBFeZaDFxl6ruJMRweh6/nhffEOGcmVtWLAAHQFn6fgZkPoD7h8f1H3P3jW6T1&#10;Fj4l+KT9qGMrMJ5LMDFUEiVbrAllF4wMMPJ9UJW2SZzP2ArIycnG13n/M1NlPgdMjLdC9j13yMZl&#10;X7mDMqvvT/oosKpBHarzU+cwOqcVGsyh6Tbs2mNk8Fy1aXf7irJ2tmhZeDBPMJgX9hm6JWdtMMb3&#10;F3BqEhxn1ZXY+fR9j7YV19W+FwNJ773GyGgy4MkM0jkNajVyNzbmNqDTZfsj45FstI4hfabyNcAo&#10;GOozJAIy5XJCCDiaH+NseYbrq2ucnZ0DRNhs1thuNhKsLv+TdSfCbcjgO28vTNTBqL3Q9CFIqhl5&#10;PS8MFFq5Q3Cxbw4hA9NxmeN7PxVcuL3g4gnJ62Ash/ymynCHcsR51WBRzj/hE0ARQBeR2k4yk2rw&#10;K0Rxd3RECmh0AkdxGzo6OsLJyQlmTYPtZov1eoWua7GYH2N5utRXkwAMr3tqXrKW9ikhcoTzDikx&#10;Qgiq1pOEXTWYqAfe8mSM+8zcV227aNAlO120n/FYeYMAJaPf+8oQCdZlibgGPcK3tXxnE7uKk08F&#10;nHgKkuQNmtwIwogsAE0IBEKP4BNmc4/ggRAYL19c4mc/+xJfff0T/Pjta7x5eYnT4xkCeiB28JzQ&#10;OEJOx1wh+/JdgG1eIFJzGJLtUpR8AbpN08eEBII7ngHzI/DsGD2RZHIEIyX1VlLAa/ZAwIgREWVj&#10;ysPHU+DjeVJ3rWasS5Y579TVD2jg0K9WePjmPda3H+FTEvdgSIIgoSYZEagrKg+YQR3Lwgj+2J4g&#10;E+4qS239PWhhzWjG7Rk9O0W0DpU9LuOHOqaY8aHr37X8Q+XsXMtzQBk9Cgmt6zm2f5kCZs/tL3vT&#10;VFnItajeO3qXgE8DIvVKrZ+mUVn6TgXOzIy27bBZb7FerbFZb5D6lLOUZrqtcyVDozHgGkCC6ZU5&#10;kOgnAMpu/xAGQbAqEABAcvXEhBAClqdLXF/f4ObmGovFAiBC13douxYgIDSSHDLHfxnVeee9IBBp&#10;dqtRO3ew0Q4PGJU2CUCsp6o+qPqIK2A73Ycj11OtkwEK41WOaAJcCNQawOzcMJRC96ozCbLPiqwA&#10;gcXP90k9FxKBEoH6BHQJ3EqIbtZEYubtISG5Kyaf5N1GfJ2TEOKX5xe4vrrG4ugY8zDDzfU1FscL&#10;kIYRJ+9AaohjuTRckG8fPGKKIFXJOYi7X2nrEFxYPw0WN8qo20IbuKaOBmlyQo/Axb57xt9TAAOE&#10;YQRRu1+T89SoU/i1eVrIODWukQeTSMHOOzgCggN8IDD38E4SeYQm4dXLK3z19T/hFz//Z/zox6/w&#10;6sUFThcNjjzDU4SnhADAE7Jrcb0SOHey1i9PPgMXlFEeE0BB+ylF1awEIHhwE9AS8NAzeiIcNTPM&#10;Zg0YQLftkGIv8yoHDnODRVXAxWC2Aztg4bAM+xwiP5YETAMo4yTEFzEhgOC7Dqt3H3H/zTvE1aby&#10;wuEy9mANy44cPrpIkal+c7UwtTWVtJqZR762y7gO/b0PsDz374O05TuCjXpdferYfGr5zy1nv0ai&#10;MP/DYGjEIMZ0g0YccViSaq0OXq1+D+vNo3N2j4EK0UbqkjUNdV4elQsli1dJuxWvkna9Rd/2IO8R&#10;QgPvApzzAoZVkWlOB0a3oJoKx7UvTCWi0Lh+E7QW9U210KP9SaVPGAlwkk/Fq0Dqg0SbfvnyBa6u&#10;rzCbz7DZbrBarzQrq4cP6jXJogWxNUg2ViDtK1fouNW1rnP+pzXfAQ2Uz9fPD9qD4Xob8/SpqTMu&#10;jzCuG4qAq2Aj4KmDMZho9Yvs70xUqB6q0gmiqRAJGInFyyMmcB/B6vWBpOprhkjH3oOgatck+85M&#10;BI4JkRk+hAwwUow4XSxwPJ+L8ZsOuvVUAtBtNvj3P/0JDMbV1RVOFieYz2Yau6EBNIpcIJsE0En1&#10;vIMYOT5EHU/jKUL1HKK07/cOis+/eec5IpI2Of2m2nraQJQwlhR7gC3XhzQqcQdmGcujmYOfOZyd&#10;neLzt6/wxY+/wMXlOY6OZpjNApg6FElapyKXBHXeBWTP86ppiVJhjmTnoC6W0HgkMs9ikoyOs0BC&#10;gLxDywmpj+C+g0fC0dyjaRZ5S2f1uMK27dQjwjQ8Qw8m2fP9lNA0n35Mjjkbu9A6JJZcIczoVxts&#10;Pt4iPq7gU5IInNo3RAyvk1XKIDDiXmlU/hyDiyEoSigA4ylAUGsmDgGLQ2V8yvVPPQ6Bin1al089&#10;prQz+95p76ql52lwxYN79gGYcbmH3pvrZRouFcnHXnxGD+wPKVfsnGpGZ7G9hf67vB3CTlzPYVpA&#10;Jg37X3iDS0JfXJB0BYkj1vdr9G3EZrvF6dU5lmenWCxO0DQBHERvyillfpRbaXTa5qx+rLumesP6&#10;mYwAWgGkWzKTYyqL1GJ7xNQDrgQRW5wt8KOjL7A8W+Ls4hx/+MMf8Nf/+Cvu7+7BSY3iNTKovLaq&#10;g7ZjaiyH9Sjzg3R8xltg4zIEbEy0fS+f2c/vn3fIWE2ACwZT2j1dqolBLwwfVbsAkf4IGlQpiZVy&#10;iuK+xJ2AC0QBFhakydQEed83v1MOBwDOwYeAru/F02M+R1+pm3zwChQi4Ekzq7J4ljzc4WH7iHd3&#10;73E6P8HJ8QLn5+d4eX0Np6HBBcQo/bXJScP2SVUn+iAj4T099AmDdAhgTBGa4fXiUgwUmDfE6ZIM&#10;TJQC0ioJqc4AJFmY5ArpBRSgx6yZ4WjR4OLyAj/+yVu8/eJznJ6dYDYPcAQkimB0iH0L74R4gB2i&#10;WoN550C+QZXsFLnDYKBMvY/YZRuM0i5GUkISGofGBXgXsiuDA0ApgUgMIpkjiBwWxw18WGI2a7Ba&#10;rbHebBGjXAczyhYeDdHOs4+xLPe8MgYGdhVhJGKQY4RE4G2H1Yc7bG4f4PqExjQPJNt7RJI7BArM&#10;pYwxObU9bAV3sP3+Yi9hBEtO6HWuSqr+ztKr1qN+mzEn+3t8jM9NMfhP3SrZJ+HvaBifKPdTtBvj&#10;939XoFLXtf49jNRiRs6jdZ5XMutWYyFYslumz5lWbljrKvNs0qibKoFGkUDZJbATypESw7OTQFmu&#10;SNfJSRZex676eFGNpySB7bycsyzVKUXVHgIUJYInBUJMPfo2Yr3d4uHhAY/nZzi/vMDpcomj4yN4&#10;73SuA9mxxnoogx35PWWUPZ4L+2ioJBXenacGWEw3Qs5pzp6IRJLsrDlqcPXqCsvzJX7yz1/i337/&#10;B/zv/+9f8fe//wPb1RbMSemzjpFYYct41+/kMsL6J4YXC3IimAEuTax9VOCptJMm+mfYH98P2DMm&#10;bC6EaKQDC4Qy4ioVtd+qak6SA6GOpNm3Hbp2i37bI/VRczer0SAwiNFeM2HSCVn/jjHm+iUwuq6T&#10;rKwkyCSoEWJMKSPTbLQHwma1wXazxcP6Du16i4uzCzQ+aAxiQfM5Yqcm3dKmD1VUIw1Cvlb31jOk&#10;jqc0E8/9PaxL2eYothRSN6/3lN/SdYG8ZLOTON0AMcgnhMbh8uoc//RPP8LPf/FTfP3Lr/D2R59j&#10;eb6A84BzDAoEOHFvbIL4wFqAHUcWNtcJcKSyaHLNSY00q3OsQM8xZKMPrO51CY0PMi+iqHfJe3Te&#10;4a7d4uOmQwcx1nUkxJMImM9mQqScFxBracFJAJkR9N2RemqhjZH+MOX6+GNHnlY8LIo84Fm0Fu3d&#10;A+7+9g22t7cIZtzMOjY6H51uZRQvkelaplT6WF7FecukSEOFaB3autjpgSzBDsFFPVcPAYtDZe+7&#10;9znP1e8dM/LnlnGoLuO199xyDmksRStABbjZkIw1Qyj8yJif2EgU4GH35XccqF9t4EvVLKnv2HnC&#10;xt3qY+UA2S16wDP1JlsjUl3b3nNZa9D3HdaPK6wfHtV1lRB8k+n4jrRfEQ6ikjhsanzqo+7zuuwh&#10;8+XclnFwKkDlkSoGiHiNBCwWx7i6vsTrN69wcXGBlBI2m01OkZAFmiyyj+pbf+e2TY1LNV6TvGDc&#10;5iHv2OUpw/LH8zrXuuZ5NPwGoWguBoW7SjLJBbkc8pRZ0WtVYee8ojESAhcZMfYSojtGxCiui+Zt&#10;kaO/GdaqCVnds/m3IXsUTbsxxyDJcQCN1aAPBY2j4ZyAi9evXuH6+hrr1Rr3d3e4/fgBy8Up5s0M&#10;gRy61MM1Dm3qEWYztF2Ho+Nj9G2Lvuswb+bo+x4B0kZzb00p5X1yI6i1JLIDxLB7DCa+lTG6vrM4&#10;RsSyMIYCLOw3gcS7gyCaJC9apchJXKc8ZcbErgNTRAgey4szvP38c3zx5Rd4+fIax8dz+MZDdo/k&#10;/kQMaPwEkMY2IAJcAJiQyIlnCJEKy7pwVUNGSBXBFDBS2oxspS3p3ZENwsg52xsABS/bX31ESn1F&#10;gMTDyDrdw2O5XCKEBs49Yrvt0EXbDlFLelfG7nmHEaL9Gr/6iFHyFDBz9gwhElc1AiTRHhz69Qr3&#10;7z5ge/eAEIGGCBRFqyR9XZhMXV+Nn1UxiJwtYngfAAl0ZfVWDUcaztn6G6hA2UQfEUmsDfu7zmdR&#10;oY4JaWzivmHBw/vGQGXEBO1Xduk1ho3CiMftykaDo2GfAv+HAMRzgNCU5LwPfBFknEg98Xafk7vs&#10;cafLTOiggnIGyOK4VH1Sb5FaFzEgWY5R0WkSn61EDMcRRBpd2amxvgVG0ey35iHBRGAXkSIjcdEO&#10;S1RYD6+aEfQMjlEcxxLELs8DbVzj/bbD6v4BZxfnuLi+xMnZKfxMYvlEhnooQsEYW5icAf+t6bL1&#10;/z6tmQGTnH+Hx9f1bwDkyhZi4gioMMcstmrLiyWOF8e4vL7CZ5+/wa//96/xm3/9LT6+vwWiaZZU&#10;1taYhbZtZUknZTsor3Kdxvvmn8XsqdftqDMwaru1JQPx/Wvf5o04WUzPc1YBZmdbRCKTQXNDuPoJ&#10;CbEsMz2jLAM5bJJOjOCtAAlWq1qkBJELy/1VsRqqWju5QqDTDZP/uLomYMg6RCeASlAOLANN4tI3&#10;Cw1Ojxe4PD/H6vISQeNopBglIFQTwOTx/vYj/v7ttzg/P8Pp6RJHR3MJZc4S8jaYRT8bY9L2MMN5&#10;j67rkFJCo4FiauCxf2/MmrdfIzF11Nectto0PkbcCUmNohShax1m6mPepV5nt4NrgMubS7x5/Qqv&#10;P3uD169e4uzyHE3w6FIEOY1NR0nd54xBSYArMeit0g6zxTVwStx22+ByGSj2KyTaFcv6B9KdX1J2&#10;qQa+DIdEDn2KaGOUmUakAX2SvUBtd6TsxeIYs/kM6/UGd48rbNYtmCU8sblX1mMzZWPwXQ8yol6D&#10;d5CGvgYCnGyH3N5jc/cAdB08mXu02IQMTe5gOE/raKe4+q6NLTH8Xf2f6zNBgPOr9pzfuQ9D47iq&#10;qp90HFovdn0IEibeVd8zIc0N3zG8/ilj/kPcu9u3+++ZAj7GZJkLUzFpVBUbui45A4zaGyMxgzTg&#10;oN5SrXBhhKTricAgpQMWaEtqLHGLEqlRuAIjJs4CIANIcBraXs8nKsH/mIEExNRj1T2g37bYbrY4&#10;X1/g9PwUR6cLzOYNIhK61INhwaooR6a1YxhbouqriT4s16E0XvkUSgA6ogLaBsNECm9YNDcJBHjG&#10;8ckRPv/R51icLPDy1Sv88Q9/xL/97g+4v72XVeIEjJDFfmJIvhaGRrfeZ5szAqvVWNb1qncbxkcR&#10;O3b7ZrJf6g6aAMT2bKhPyosKqrVJSBaKmEWqqgNkSycyYi9AgvoIbHrpIMudbP1O04wFQIkRQRWS&#10;qq4N1LWKzop0Uu4Tuql1VY5EabhfFpnRkMNycQqvSDNCbAJSStikFt9+eIc///1P+Nu7BmenZ7g8&#10;u8DZ6SnOl0sE5yStMLEG5vLZK8U70WT44OHJK9NIuV2Ji1FU1foy0a37MRzw8e983p6zsYKILXI/&#10;ZyMiR6GKbucQY6dACGCf4B3h+HiB5eUZfvSzL3BxLdkBT09OEGbiGhY5ytYHpYEVthwaaUajM+a5&#10;A2C825uqJ/KYutJC8yaRLa16W2yC0YlqAz0D6xjRxiTvZyoqf51QskClXwhAcAFHfIQIAVzttkeM&#10;KT9XSwiFFk8txboPkG0p0p7bgu7Vgut5zVlKnMHh4WGF+3cfsX1cSbAsIjV2LmCBbBevAg2sVR5v&#10;aVS/KknX1qfZWNg9Qw3Mvu2R/QCjYvTVLbVK99lKoaq8MQAo54rUflijMH2PlVEXPa5ezXynfu9o&#10;Yw7gi/rZqT3ySueY60cqDRe4MGQ04/YYfSzChQl3RreLoFMLahapk6C0XjUdxJSjfZJuecMJyHS6&#10;RceUkBKBvJf7nbzDseT8SSwaEe8kFw4zZ2BBjjRRIeWAiezEjsGxAzvG+nGDtuuxWq2wuDvF8uIc&#10;y8szzBdz+OAhsm0COQ3yWPXPIbA8NT4iPBvPIIjBqtAUCUS3K4AMyhDXRhGEND5PM2/w8s0LnF2c&#10;4c3nr/DFjz/H73/7B/ztP/6Gh/tH8ZQhhV3MCI0ApcQqpAMa+LBm6koLBnx+vxC6U082rdWY23yq&#10;CLB7DGwucgUsIE+BbEKcofv05l6UGKlPiK2kRI9tB7RRwz8XiSzvY+2rhUW2HK7vnYNG9wBDsEL1&#10;OVOlj8sAcnIrBwtrTTlBVx8jXBOwWq/w+LjCdrvFavOIu/s73N59gIPH9cVFJsyseVNitXed3Y/8&#10;cG9f3j2tkRgnQKsDddV7W+MEabv3S24QgoXyVlKkHZUoIXEP8oDzAHmGaxwurs7w1dc/xf/9//x3&#10;/Ognb3H94gqnyxO4hsBISEjqqcG5o4fAolDVRL6c46J1UPKlM6G2PNdrrvSFgE0NJAMDKQV5MWva&#10;OHJgCmiJcNe1+Ni22CZCTwRONGCTycArueyLnlg8j0JoZKw4KtMfSgeDOXiIc6CApn22oabxsvG2&#10;FOcO4hnjY8LD+/e4/+Y90nqNGUSbQRxBlKqeRt6eqwO/5fHhside0MPYE2SCiIzOH7KVeEqrMHU8&#10;Rd+fa1+xVxLbI9U/93jOY/vKNhue5z4/Vc4UrRxqN8s7xrSk/h4Al6zRyLBsqlbAYHYpo+Ua4HB+&#10;NGurK6Zd2IUBXq5Qbz1mNQCtIbO6lHKJzVLsM+R330VsNhtsNxt0bQeCwyw0Eh2TilZ2WD4GdHOq&#10;38oDRaCA22W8RhsGPTcYH4Vn1p2q5RC7jYSmCTlfyes3rzGbH2G1esRqu0bfd2Lw6p1qjiJiFNDh&#10;LdBjbs9o9CphCFUdC521j42U8dQy38r1PX2DPJUqgbbQS+NBRBW4qA15DBA43auXLJkED1Fbcc/g&#10;yIit5PpIXS+ZSaO4EnrTbFhjdudT1RAqY/DEx2Zufc7xxH0TzNsAjoTHli0NC/VtGfsKkmecLE7w&#10;4sUNzk/PQIAE9UrAi+sbHB8d5RTjfd/BhwAQKZgg2RbpewEqziF4rxofDLLElmylJagYRpN/akEc&#10;+l0PsHNOM5QCQBRJwjPgEigQZosZXr15iZ//y1f4b//XL/D2yy9wfn0OP/Nq5SmLgcxzBLoFZJE0&#10;7ZOjrbrc9zLhZDtMGJp+ak8kqjzVbSXDZSBkKlbHxW0WTgEDif6MnQO7gC0z3nct7vuIlgmRBNjU&#10;OTGSEqw+RqQYdR+TMugL3qEJOoYaEcZCgtc2C7vcw+puZMUAHSYPgoIJcvBUbC2Cc6DI2NzeSZjv&#10;u3u4mDBzTjP+qhcPACJTSQ+3Gm0Vy2fMQPR8rbEYqiy0kD2gA+Pbvr9089zj0Ls+1VDzkEGm0ABg&#10;QHwPHFNA57mgZgpgCDO1WXnofofBwFd1qDUWk/UZPTc+xGhabpTZbOtTaSQEBHA95wx0V7Q2V2T0&#10;qgJb7N8ICJkokVg9y2Qui4KJFHgwYicxMtr1Bn3bwSXC3EuMHuakMTEogxsiKtE6rc45IpR9dN1P&#10;qNhrPkJk+vthGgHrEGuj0EvWCKMV8HJicH50PMfF5QUuLs9lSx6MPkb0XacvVZssdeXlFPMc4SpV&#10;Qq0VtDGwOu/OpgKQ9gGu8uyo7RWPKSOFqrwCPnZtLkyiAgliRKbMkp45JvRtBKIQ3RQjSIxfNbW5&#10;lKHpKLI9RVbPHVh4dXyDpw43sTZqYJENmsbXgZydb+DfTdAQ0R6BJH/JbHaM0xcLXF9cot1usVqt&#10;cHl+DjAQU0ITAnzTIHLCuw8fELzHyekpZk2D+XwOs8dIKSE0QQ13CnO1ARoHTKnVlAZ4xkBi0K49&#10;E4QcQB4Z8bvg4GYOi+MTvHn7Cp+9fYubV1c4v7jA8WIuhplgUWtCn7Pxh3gQuT1J2fI7WR07kGR7&#10;RsGEBfcSAmRUqa7v0L6HnRqIshqjablRwR85D0uUBeew7Xq0MaEHgXX7g0ksGxNL1t2+F7faqNtk&#10;Fp01JX0HJJkSE6Hve/g+om0BQJ5DluB2Wn2wT8YHZ+NjyrYX4itP6Nstbv/xDpvbe1DXwytFitwD&#10;kARmzLEqzECCGUbXl3bVwLK+K6lyqn465t9V8t93/NBg5JBGZer8LhOfNqx87rOH6vDdtSYy3jWz&#10;3S3rcH1J19m+PXpAyt8XzYWrby4UKT9n11x1b0oAnGTv5cRmZw2ilOtT2iJPZ82aK3YKTLLtGs3b&#10;gyGJ9OABB/UsFFqSYkLb9ehWLdZ3j1h9vMfZxTkWl0scLY/hQxFebX0MxqvqxsE8UIZda2l2GC0U&#10;hDBnt9VStq5v1TykKnssiTI7h+L3jcfZ5RKL05/i1Wev8Zc//xW///Xv8ed//wvub+/Qblt9H2v8&#10;IWSD87yFPpIj5F2uzKHqeun//TYYU+0dXCsXKi1K0XjY9QG4sImNurNYtBTcJ6SeAQ2ARRr4yrFu&#10;gRBlK/+IKJMLBVzkTjgwqQ0VP0NomlxeDiKpJkXeOfUvD5+ZzWaDbK6+aZCIdb+dMXMeiRNin+CC&#10;x9wHHJ/OcDybYz6bwzmHrm2lDCKs1mv87t9+jw49bs5vcHl2jrPlGY6PjjLQspx4zAxfS/41aGAY&#10;Z97RTNR9dAhRAkDwHin1si/IDCLGbN7g5PwUp2cn+OrnP8PLz17g8uoKZME4KaoK0CN2sYAvsr1K&#10;EmmZSGKUaI9jMF61VkLV9+rW7AaLmiAaCiVYVEfF5MHgWr+YRMkJun2iqlIQIgjbmLDpI3owIsQI&#10;KzIQEyNyQp8iogI9IwraPdn7pE8SMTB4ByKfffE779H3vcTGSPXk1DoPxMSnJ29N6I2AEYC+7/Fw&#10;d4fHj7fgdQvPwMAOjYCY1ca74Fm7RmLJDAjHbp0OMnoVRfbdwaPvncdHAOf/lOMpo9wxc5i6NgYV&#10;+0DGU8fU2q6uDuoxBkLDek6BH72mzFp0asPrRJR+F25VAAAgAElEQVSz91B5SEuVdWi2U+Z9B5Bm&#10;Q+Xs8cFQY0wDC0mMGImQg3SxWWGrDQY85QzVKYm9F8gB3iE5yY/sSPKJWPwNMIEigTjBe9EWUCI4&#10;D4ATYurw0N1i87DCyWaFs/4Cx6cLzOezQe4gC8NtkT4NcFjr69GYBnXDbQUAcBDPL9agYqmigxJn&#10;KSEmEQiSalQcOTVRM2HeYXlxiq+WP8Nnbz7HH3/3R/z617/Bn/79T9is15Bki/ZapUNcNETC5821&#10;l7KQ/SwAMal1+y7rtgaiPOGKCsrok6Okyk1dRFJXIbBGTGSUbZNKQ8dWFlUol4aDtq+hZb/4ibsn&#10;OiwzofGtE6VxEq6i60UmmG5bND4g9f0gcVTsE9zMYx4aIEnsBq9uWO12K8Q8MR7XD2i3W3zz/h9o&#10;mgavL1/hi88/x8nJidwXE+bzuRh/whbxiNAwAF9iGBjCt3v22WxQ1kclJHSI1IMcEOYzLJenuHl1&#10;g7dfvsWr1y9xcnaCo8UcFAg9RzgNdBGjRL30bgageAfZOBogc0+NT976mHbNtOyvSdFziX0jvZJV&#10;wyTGw5aUKz/PEh/FSGvPCR1HtCkiMaFPDMHBSedwElcvBTDmqikW7ibxk2qaxcYEjuAhIeLJi6sx&#10;deJSHVMCcVlIlEcSuaKHNHBMmoFVt4rADJeAdrPFw4dbxM0GIcUcdIgAwDx8NLooiVooA41cthL4&#10;PBQjiTt3czm7U78phvldQMIPCSzGdfo+ZT93m2TqeK625FPe/6lHDS4OFWWGicJwxgBF1ldhqjR4&#10;0iaJCUcZyNLoTr2O7JXlsqceQBrqGqJNhEjdnIReJbY8I2q8aYKnWJFqNE4la6aBSCQhDRzBSQAc&#10;5VeyLpkTPn7zDo+bR1xcXeL8/BzHJws0sxngHJh7DFpt8swALHLVBYV5ccFyWdssuIeyBoPB6qaZ&#10;cjymrLUg0oCFnPsCzOjVdT7MGnAEzi6W+Pkvf46blzf4w+9f4/e/+z2++eZbtJutbqHq9oOBncH4&#10;KTSkIrQMxYyylVrowdQmnF369LlqSTH9L77+5a8caXDuBKAH0EbETYu46cA9gyLgEmugHlJ3mdJp&#10;RTXBEvWtksbMpqC2rODRR86hBuLPOgpD1QpQATciLe8i0awdGTFpoGaauqekhqZWe4c6yIoiRhDm&#10;sxkuzy5wenQKTx7rrsP9+hEBDd68foWGZJvBa5ZY571aEUOkcLDm7hCDWfYszMkJ6JnPZwJ4vM+G&#10;nwS1g7HIogrynE/YYo3ZacCLNzf4p6++xC//+9f46S+/wpu3r7C8OEWYecBD05vXSJUAi0eRHdxr&#10;1br1s9pWKPCQunJ+xsZAwrN7WFAbi5MiNhtiVFtG32ZIghh7in0B9MMkjmApRTS+sW1UIDTYMOHj&#10;doO77RbrPqIlQpsS+k60FUgyb6GoXggdIcEpowcSm3U9ZSJhXULeSZTB4OGCQ0pR2q1xWzwsbwpk&#10;K8kUMdpn5v3CbAS2aHwCeczIAV2Ph3fvcfePb+G2PUIyS3q5TwgvwUJ9mbF1meNcUT8DnJUUC+Tx&#10;KQadaXgN5XOIiRIsAm9FC6oP53Kmj30M+RDT/5RjauvwU58vhG33M0hqlfta3a0Z1fXixVIfbENl&#10;jH/0yZZIozbsbM1AV40FjYJuaOsAZ7u36plxG0m1iMr9ygwYgbjidYK8jvLUyfSwOpcZsq4lM9oH&#10;a/yIAnQVIqtNVPFesHcNGKECpYRirGzbmvmlMSFte2wf19g8rNXoU2ziyHsFV1IXB12/ADyzGFTr&#10;mnbW4Iq/5N4y8ogKYDjKXm/E+eFq4FFR79pGC1ljzRDA1cwbLJYnuLq5wuWLKxyfHCNxwrZts40g&#10;VMgSTuAk4aSGl/YkNAlKByxG0JBfG5MBTHsieIpzx1O1ljNLBHILzNuyHjPDZAEM8fpICbFLoD7B&#10;9Qx0ERwlfLKhzwEdqg6uJl99PHeBZxyeB+/wc3vVmvbe+se4LhMozdBdvsVRVtkDQx9pgmwLOBIi&#10;6lhAw9XyHGeLU6zal7jrNri9u8XZ/ATHsyOgj9Dtf/QxoksR277D4vQEIXjJFdFG9LFHaDxSjJgv&#10;jtF1HRyJjUjXdTgJs8LfGXAaTZOZEbkHwaGZOXz+5U9w9eoSb9++xc2LG0ni5p0yvQl3LAYsFsVY&#10;5Tfou9K7e66NCOCIyCd2uc8M4WcZu3LtsjYl1RLlyHkaVAYpwcOJrUXb4369wd3tHdq2A4WA2HN5&#10;t2on+sTqGa2gls3nvzJMU2IFk7Yqgsq+hDyeH4nLW99KtFmxaodqOgAms1WRpeYgrnlRJ1lKLBJM&#10;jAAkj8jm/gGr97dImw2CghbxClCNnEUXyqTYNgF1vCpmYOtnlzmX39lCn9lQEfYd0/PlwH3P2Bra&#10;957vus2wty7/JccELfoe7+fJEofHIG4DGxOHAkrVaCi6NLAwtVYFDA2Hs7bJmQR8hloNQKFssThW&#10;ewISg0WhreK6aYEOiTSfiaUmp6SCiINzrFuQUYQmR0g5UaV8PByCbp9E1f4ZYOl7kpgwREjbHv2m&#10;w3q1wv3tPc6uLnB+c4Xj5QLkxbVetnFMhqGM8BTeQS28YFsN1h9pH3gmDWvuDGyasF3fP0WDFYQR&#10;kLxsM3nnsLxc4uT8FG8+e40vfvQWv/317/DnP/4Jdx/uELsI0nDrkkLDqR0ZRENjgkUGGSOBoxI8&#10;DNqQIgM2DU5ploxvFqorMJEBh9J/FTQDtbIPHWOUysYkBDiVlxvdNwnskFRik+c7H+OZ/kT5k2pf&#10;GHraNWraV9bgykBFVsrNWgNFv3bNGGEIActZg4U/xeVyiYYltHlMEnSLvEekhD//5S/4j7//DReX&#10;Fzg/P8eLq2sczwO4Y0SBseAobr4zP4P3AcEHEEEAiA+Auob2aMGOcbQ4wstXL3H9+go/+erHWJwt&#10;cLo8RdM0GiG1B+CGSd1+oGPX2AkZxGVGJyhWNBDJFTBqBqIVg0+6Hxu8g/iXy1wUT6QAxw59H9H2&#10;LT6uNvjm4x0+PK7Q+QA6OcVx49Eyo0NCzwk9SQj3iOKZAd2OsW/bP2YU4I6qHflvBzTzuXidOI92&#10;26Lv1OCTRF1rjBsgjVpOpZ+UCAYSrZOLCWnT4uHjLVa3d6AuCSkre0UKMjjXy5ah1WtHyuTp7Sg7&#10;ipugnVDSkHOtF/GmgKRxCRNzaArVf+LxQ8zN/2pgUjPh77vlsa/cul6H3iHM/3Abato1ZcsxBLZs&#10;cke2owPKt8uIRiRZ0TCK6t5nZiUCrL23CcI4HUi8oAAQMSgltUOAaAAcwOwLAFamnGKPnqMmJPRq&#10;w1DcbB1JFmdERuoi+hjRblrJvrrZ4Pz6AouzUzTzGVwISJzQ5boqiGcuUY6T9okuC/OmGQIy0Xhk&#10;jUUV5Xf8KeLs9NikKC8MISBGAU+nZyf4yT9/icvLc7x+/QK//fXv8Nc//w2bx022f+n7Dp4k6ikn&#10;sRyVMZHd1MRRAB6pkEbIdiJ5/ugarsw7hloN2JZM2V7Jij6dPKqHQojrraiVUpKkTwlZai/5OEZS&#10;BQ5P8H3HIank+5Q3/j1YKD/gYrc+ZBbDv4ykNQS4SbBgYDGbwyeAYxQgRwAgbpAfPrzHtx+/wYfH&#10;W4S/eVyenuHi7AxfvP4MV2eXktitZ8zCXN2xZO+ujz1mRzOk1CMhwgWHxXKBm5srfPb2Dd68eYPz&#10;q3OcnJ+ASQwpzVLZhwAmDPKyfO/+mCCoZvvimAY8KQfdYlGSZIOyPCkpS0W2/cSxPGTSOzHwcHuL&#10;h9Ua9w9rvFut8GG1wWMbkZoGbh0xW55JQDQHJA+wYwEXJO92miiPUhGyxzPSeHsmonaTSgDOOQT1&#10;LCE1+Exqe1PUg5QJZ1Y/smwxOgIa50Btj4e7W6w+3iJttwiWRCrXo0I6uZKmdSknn2KIg316Hmku&#10;hnfCgp7t3xqp0df42g+75v5PPMb9MjSy/OHf9dR20XgdTgG/gW1WVe74W99gL5K/kqwbu8dYI0GN&#10;5xXQWAQbwPb0UzbGdirROyLEvkfqOIcDsBDiTA7EVRA8fU9itc9QoYMI8InQO/E0MTps72HvNH5M&#10;guMA3wQQGO3jGrdth/X9PU4uznB+c4mT8yVo1oCdBPiS7U6payDdWndqh2gAwdqt3eVM8NH1nRyQ&#10;KsP8WgCtx6OAjeE4OrXFSKmXYSAZ02YecPPqBufnZ3j16hX+8Ns/4N//8O/49ptvsXncgILQmhTV&#10;bg0WIZlhhmbZTEHHrcRMGUhUZRQJutUCFbQqbUUWvEbzjeS6/+qzn/wKfYKLLMnGUDqlBhcFpex3&#10;33LDuu1M7H3HDkjYe+f+Z6beN/58SjlTz9geplk5m4RdT2znHDglBG97UYzGezDEa4GJMV8cSahX&#10;AKvHR9yv7rBerXB9eY35bK7o06PxjSDiEOAcxArZMSgwLl5c4Ef/9AV+9vVP8dW//Aw/+epLXLy8&#10;RHPUwIJd5jbk/U5rCB3s5B05dYKY7f2w9JPZAnBFfGxRkgYsq6N8stbLKUrmlMARalPiEbseq9sH&#10;3H77Ht/89W/4+O497j7cY7tp0XdA6hP6bY923aJ9XIO7Hs5L4JlEhAhGb8mHbFHY4sqK4NzgYcUG&#10;vJQ1rLi0iYKD90Ha5DRTKfNQ+5EXq0pWzHCJMYND97jC+7/8FeuPdxKJMybtA5Mgi5TgTIORS90F&#10;FkIw9jC7vF0x/q7rufspmpjhe8fvztf/k8HFU4z8v+L9+7Snn/LuvbQHu2twr4Z2gr7Z+OyAfqVP&#10;43J3NY9Dm4BKLM1/ly1HfeOg0tXCBsFstLKdhnMg1ThIEDyzd0KOYZP5jmn9UDQSpC8jMxmqNC5g&#10;9QgBQF7eK7mu1MMxJnSbLTarddY6MgMuCAiR4ixQnREtrVNFt4zhu/rDnL3iWNthfZWB2YG5UsCF&#10;GLTGFEGO0DRBNaKiGZ0fzXF+cYbrm2tc31zheDFHn1q07QZd10l7XACZbRyMlkiGWgmEmao5Vm1N&#10;VLzDgUSgq8bcgMkQXJQhr+8JLqpRiKmHqcwJoHgITGkwBp2iOhRDtVOd95zDAMyz738C1U/d/1yD&#10;sp3zisJ9Ngoya2d1b4IZVUrIVhLYDdcEDaXr4T3hanaJi8tzbNoWHz58wLfffIN+2+JscYLj2Rz9&#10;tkc4mkm21xCygeDZ2RLhKODmxRXefvkWX3z5FqdnJ6AgMSzYiQuiWFSLloLZfM4pA6G9+4XP7OMx&#10;QdvZPqqKZ+asQhS1arFnqcfBGZHQb08BcEDsErabNW4/3uLu249Y3d0BvWiOwB7BOczh0Gt7Ux+x&#10;XT8Amw6ubeHOFvAnx5jNZKH1YJEKoDSvMltA/aeBdR7Wk5gLkbQF1ng0wYN7D+c94not183uiste&#10;JgEaypuRti02d3dYf7wHb1vJccOAGeTVvU5Zyskqi4Hmou7PffPb9lqfNokYM7Kyqktb9h/fVYrf&#10;J5F/13L+s0HGPpp46P3P0nIYcxyVOaUprN83OFf9X7+zlp73vl7BQu19Z5Y+ZT5Us4JqQKt3qhbS&#10;6ir2ThEREkGYHYmwpeudoPRJQYNpPMi57EQQOMKnYcZhz4SAEhSRlCE6IjSOJNlaTBKDyeZ/YsTY&#10;4qH7gM3jGov7RyyvL3BycYb54liEOtuGSRLuGxqwKtu2sDLYcd9lPML10hyMnY3BPs1gVENrp76n&#10;Xd+qoatDcAE9i5b0ZHmM5fInePnqGi9f3+A3v/4t/vKn/8Djxw24le1XIonn45KrXNSLIb3YvMhI&#10;2lnAvINoMIbg3fbWB1mHEKSunnTwwGpQWUkpmvzFVCHWARlhmapk2I9F+zGxhnh8nmj3vmfShH2L&#10;l564Z3x+cI92dN1mO5ym/e66tmQENOM/5UiSgCbkbHqJIyJJPhNHGqkzMZoQMJsHnH92gs+uX6Bf&#10;t7hangEpIQTJOhrgsek3+O1vf4tmFvDTn/0z/sf/+z/w+edvcLQ8wux4BnhGz624M9l2ApuaURZn&#10;qPb/UkrFzmHcJwf6aR/AmDo0XQ94wgdZjLQVhJAZK1IGFaReSX3XYrVa4fb2Hncfb7F5XIHbBOIE&#10;lwju/6fuzZokR5I0sU/NAPgd95EZmVU1NTPCEQr5QBEKj12+UnaFfCGXInzhD+byJ5Az0tPXdHXX&#10;kZnhtzsAM+WDqhoMCI+IzOrqnl2UZIU7HKcdap+qfqoanZC42oA2BsQ2AlGyr1ZwOB6O2B/3wH4L&#10;v5jDn81QVSVcQWhJuC2CZcy/2Jkok21AtRORSZzAJfV+1zOIURQOcAUCj6RoX2PhcZTq7KSIqwjs&#10;12usPz2CmwYlObgo2W2RBECmkdl9WOZobj0gfY/nNsYAQRF11pnnzkk+Lc6+G6oxCaLJgcADQfnl&#10;i/rJBfIZC+mXXPMvATBOzYXn3Bc/5/4JfA/ulSttlB3T+627iB3c0xYlX41+79BB/zvZ0pNdVxeW&#10;LhX/0y1/ovwYZo3u0OJcRxbehC88irJAUZUoq0qKPJaFlE4oMsCQtGHjMHFaKNmCEZhxjEHqWzmg&#10;QIRrapREqJzDyCm5tI0ovMhEbiLiZo/tsUG72aNd7nB2JfkxinGlVk+HoARtaw8yK4TyRWzRtVpC&#10;9vdZC3+mkPa5GIBFcJllwK5hdUZCEEtLoZmE26ZFNa3wd//w97h/+wa/+81v8av/7zf4/g8/Yb/Z&#10;o21qgMV174g0TL0f/p+kDem4UX6XtHtn/Xg6z22IUadokXEyCEU3gjq3B1lFJEVfElPsTl84/2uN&#10;cWLg5cfnk+Qvub0GLF4CGET0JAuoHVMUhVprlKtCnbkx6kSKYFRFKZ3kHJwXJBpDwLgaoQ0BxBGl&#10;LzCqxoAfwQeAvEP0wHq/wuXlJbarDf7f3/wjnHdYH5f4P/6v/4Cbtzc4hiMCWgkH8h4tW4y1JAAr&#10;XJn6ocvRT59luThl9u0JtBN+wjQ5bPIDGlSq63KWVc6ub244YjH5Rcl6hY8fPmG32WO1XGK3OyA0&#10;Wmc1EDgGFPCS/tciQKK6Y6K4HGLbwHlCBUa92mO/O8Ktt6jOz1CeTUGVBxwjMCUTKpOAZbG0mKDl&#10;3FDwFGCT3C9wRwclIozGYwE9LgBNizZovH5gUFRNKrTYLpdYf3xEwYyKCFzXSVvJwQJhqEU+h9qR&#10;3CJ5Xw7Rez4HT/d//s2iVOwmeUtk1/wcjfzZ+70AjP4TBhin5sNfaztlHck1TBmL7pl317nYl97p&#10;ew6uAaSFaKhu2bhMNx3IUHFzyPyIDLiyRDEZY3E2x3g2RTUZYTKbYjKdoBqPUY5kQXeFRD1YmYY0&#10;FS1BDbTidowIbYvQBjRtg6ZuEJpGfqtbtIcjYivFDJsg4d0j51G5Aj6SciUcEAP29RrN9oDD4wZn&#10;l+c4vzrHeDGDH1cg5xFcV6wRJmu6r/Kvl4/nqXKa91E+dvr/5Iq972led1FCoii2cAVJXaQIXFQX&#10;mC5muH/7gN//+jv87je/xx//8B0O2x0it5qBGmpekWyqeX/3FApNOpVXRucTnzpgka0Ter0Cilhs&#10;fnRjRbXL7GWsyejEwB423nPfP2dLCUqy5zi5nTAVPndOftxL5ldBpF2s9anr2PMhu2a6PgCKQOmk&#10;wor3FYxcw5HhQSjIWcwWHANFUYIh9S7IM1xV4OHtA+7v7/F31d8BY+A//j//N27f3uDu4R4tt8K9&#10;sOHLDA/hdYAB53z/HRVNgggvabj5+6bwz0H75u2Xm1pz8IIMmQPiK7X4bwcAUV1KEQBHOEhW0N1q&#10;jc1qjR+//wmxadHULUJCJ5JtU8ySEqLaxoA2OrSpYqFoMp4BtBHgCA9GUTdo6wbHukW936E4W6CY&#10;jeCrCjUHMUMSITphaqc5ZgqdAQxSC4H+tUVagkIzYeIIVHhUTtwkODZSK0AFxMh7PP74AZuPj+C2&#10;BamF0JFdMddHOX2ycZ2sJfol9SmrRtKr3/K0n6NI6u4Fn0l29nTrxGmPIJppNmnrO+G73Tlas0cc&#10;zrNn/Dafu4i/5EL43Ou8JMfyxeA512D+9yXZc+oezwGi4T2eV470+5Br1b+Y4dF+MJB2m40pySRJ&#10;Vi5VZUK3WPS6nsVGFgtCdBJ2z0SYLuaYnS8wPVvg7OoSlzfXGM+mKEYFylGFoqq09oYa6hWYmIXF&#10;dOtkNbC5xsZ/iokDhxjBTUA8NmgPNY77PZrDEeFwBB8lpD+EoPwIoIiSSdI5j1AH7No1mv0ex80W&#10;F7fXWFxdoJhNADgEaMi5cj9iCNJKTqwVASxzX9dmNxgzp7anVgxzm9icpJ6CYw3O2vjGnGAwyAGj&#10;6Qh3D/eYn53j6s0VFv80w29/9WssPzxKriQIh82z02SSqtwBIM1DxCx1qACWQo69cYPOgpVbKtSq&#10;TxxtAEqGTkNd9vnpOOwPXHmvzwMNP8f/+Zrv9UWfJvDEnGjnvCRw8v3DRfWlc4ffHQgjJ+6PGIKk&#10;OPEevvQpn3xd16jGJRwVYA5oQwPnHeaLBc6uz/Hu79/j5u0tzi8vsDhb4L//n/47/Pv/9X+GLzxG&#10;sxJ1fdR3NXBBsHRkvbUpb5TMaPmakMtJqs+1U87FMYKrtVujtVvIUuwygTWkihmgSCi9BzGjPtRY&#10;rz5hvVph/bjCfrdHaILgiQARIGpRQNR4dkS0HNAGRsMRgcVy00YSMBICSCMyCs30F4McH49HHHdH&#10;VOdzVGcLMS86RnAsCbW0sBEzgzUdvLeJhKHP2gSf+HaZO/Y8gwHn4QqAGq20CuHlHLdb7FZL1PsD&#10;XIiwLJ3UVwUBcGexoLy/LWR00DH85MPp7cXF9SkoeP7w4QLajceTwOKZC/ETefL0uF/SOvCXtpie&#10;usdzIGcIWl7aTpECP+feT+41sDB0n/VYUjESGcwhWQ8s832hERsxCMgHUbJuO+dQU0A5G+Py/Bxn&#10;lxe4ubvF+dUlqskY49kM5agCe5I1RyPHFJtowTBKz2DyvDMbGuJHGibEHaGSGDL3Z6oUhABuA2Lb&#10;ojnWaA9HNPsD2v0B7fGIpq7RtBEFJHeOZ0ZsWoTHJTb7HaaPj7i4ucLi8gLlZCTWmBiEuO01Dwcg&#10;yqHOY5/bhF7p31NrWcpaygqqVLkC5UnZBtclUTciAVQSZpcTfD1/j/OrBe7f3uJ3//xbfP8vf8Tm&#10;0xptI5mMPREK7wF2CG0U8qZTixNJaH0+FTvLGCtlAjABlQCfgQxi0P/+P/47UYqIdJFCh0wH26li&#10;YT9ne3VyfMGxp44hKxQjPybp+OzEzKwOoNw9NLjuiesNr2uDu3AezjvtG0sWQ4CDki8lWQyIcXZx&#10;jjdv3uDh/Tvc3F/j4v4co+lIFnFuURSSpT3EqElSdGG3xFe9fhHNXl+i9/zPgbKh22MILF6aFLll&#10;K+csRIKSJRlgzSNHTrNOEva7PTafllh9WmGzWuGw2yM0Ldgyvyq4iIrmmYW3whylDgxHNIHRsOSv&#10;CJHQBAE9rpVJEYjQOqBBRB0llj0QELwHxiXK+RzlYga3mILHJWqnhdGCsLKtaFj+fkOXUO4GZLMi&#10;eI82SGIgIuCwOaDZH1AEh6JlfPrDH7H+/kccl2sUIWIEyw4oWgMpSrQ0yklQsXAtKA77Y+ALpZct&#10;EU/PH/azG+zPMnlCsoU+UQBcto/75/ev/cJzpTnbf/4vBRafo0h86fbcfHjOwnDqmOfcKK8pO6f2&#10;vQReOtF3ep4/f17nymCy8FGdtyoXWcclADCsIJcs9s47UFFgdneFu795j9s39zg7P8d0PkM1Hitw&#10;UMsEGCEtnE/bKtnV7IP9zkAX3YAnfD/AstZm1UoVi8QYEdsWaOWvWCh2qHcCNsKxQWwaeI4oSWpA&#10;VVWFyXSC2WKOxeU55hcLlOMR2JNmWWYlfNpTM8jcvSfGzOe40hRTiOzTRIKdxbkPwonQaz9oHyW6&#10;W2TEusXmcYM//PZf8Ot//Gd8/9332K32CMdWIxNL5Nl1A7eIFITwnhCnXpkt0adxK8RMY6HGknxZ&#10;MiyL5UIbxohEp4a6LbivNcwvuT1r9vvM804i9Weudcr8f+oYQ9nDY+2zZ0H2HAM4BslSV0p2zKhm&#10;pia2KMsKs/kcl9eX+Prbb/D+q69wcX2J0aQEXECElJp15NBy07kgAuBoWMzWQAYnUs4wOUr+jLkG&#10;dOqfHZ/38ykehu0fLrq+KAGtU+LIoaBCTKZ1i7ppsV9vsfz0iOXHRxx2e8Q2JosGgZSkZRaLLi2w&#10;8VkCglgsopphmRAjwayJomVF2Y9OIHoo8IsRdb3HcVsj7o8omwBaTOAmFaiQDHfClelamNEf9zlX&#10;tZN/KkAyU2VsAkLTpLTC8Vhjv1qh3YnVohC8Kf5PYsAY9OgybUrforNOvbCxWUBO7E/XeuG30+cN&#10;hFcGuNI4iAKD+MQzfok7I/v2Wed8zjVfcif8nOt8zrGnzn3uGl/KC/kcufjngCoigvMeXk32UvBP&#10;MUSnkov7E1KUqywrnN9c4/btPa6/foeLd/eYLuYqLxk1Sd0PVhfk8B1IlbaTem2OmxOY6NzCT97U&#10;iUnEChYyWEqWFw4oCkjCrxFG8wmqizOEuka9O6De7dHsD2i2O9SHA3wE2qbFcbnGbrfH4XBA3TRY&#10;XJ6jmo2B0kPgvuSHkrpP/fb/OWulnCvVk1hzXjD4+UifTOHtmkwUMTCjqErcvrnFfDbD+fk5fvfr&#10;3+O7336Hx49LHLeHVPGZWMnkDvBSEU5/U1cUSSirgD6VC2Y9SlqYKkGeByXXjcj5M+e1afZf2qAn&#10;LQTP/P6aST8/7pRQeW5inlpUX5q4p8BICkVVfxiIAM+ILiJSlBoVhcdkMsHDu3d4+OoBV3c3uLi+&#10;wGgyBnuHBgGlAxBZSJ8kTGEikjoZoGdd5GTCnRxY4yBPaXH2/M+5P3pWiBNaluX3yI9l7tJ0t00D&#10;B4/SlXDkEJuA3WaD9acltusddus16sMRbR1UoJCQsPRZW44pQyxHIV4yA4EDWha3iAA11nLqhBi0&#10;3yIQo1yjiVFcIb4jI0u0BqNkhgsBcbXDtgYWGrQAACAASURBVG6B9QjlxRzjszmK0Uj4HHWrdVOc&#10;mkBdqmkQsyZjKBk0tZ9UeoyR0dZCMCuYEI9H7B8f0W72oLoVIAqCY2NsdMCAMg2R0gS2+50gbZ7o&#10;q+f2vf5bHMqqwWb2lBPaFMenE/iLt19WefmllKEvBQGnzj31LC8pO89tvwSoOCkX5IOMOXULMkuE&#10;mfNOcz0I34kJKMcVLi/OcPvmHm/fPeD6/g7FYgqMSrCXOSvvTEoo9AqgY1eTQpC7KBWsqN5Zng3A&#10;LALykAC6/JFI4zBDJZZvwkBSVHkciZJblcDwnuB8CT8qMZ1OMGoW4Dag3e1Rrzdodge0u4MQRduA&#10;sFpjV9d4XK+xuDzH4vIMo+kYXlOZE1nhtZwlcnrcvLiO2QJNAKu1N2peCnOtI1vM0zX1K6kVgUU7&#10;BQOoY4Afl3j45j2ub2/xzbff4I9/+BO+/+57fPjhA9bLJdpGMnwygBBawY/eSLUKYFjI6Sl7KRRn&#10;6sPYmzsARafdvy4TXiMj5UNh+NtL5/2c3146xg0mzfDYl8yFn2O5OIW6U1Nr8hNfSqGrllvUscVo&#10;PML13Q1u7q7x9uGNZFq7PIevPNg7RAREivDk0DQ1RqMRHIvrhCMQoUW4SKp15pspvSkEyslilVso&#10;coJm/m8IEoZ5TU5pXEPwNbxWRYVEc9QNtrs9Nqs11o9L7JZb1IejhIuxhI5Kdlgpdw9H8J4QVKhB&#10;M80Fdil9bYAItgiJBw8s1hyxWsjiHyLQMiMo8dNiORDUxM8ejoRk23IrzPLtHnF/BG8OqM4XoFGJ&#10;siwBT3KtNmhK4q7UtOWEIfTjKWJoUWma9RgCPAMcAg6rDVY/fkA4HEEhoIiAJwURxCCn+Tf4qfaf&#10;t7soDK8vmM+BjdfW2i9ZjJmHhsEcdPy8hd0sNz93If9ztPbXts95n891e5z67ZRyc2q+vXSdofac&#10;H/OSTEyfbYlgqf5M3gthj8RtGAkoZ2PhU7y5x/37t7i+v8V0PhNZZn77GHWBEnmVWoWQ6nikpkrj&#10;iF7Sn/InT58SxUfHjSW5Iy3XnYJn1U3rUhSNAKfAkIRbroSrSozHI0zOF4iHGs1uj+N6h2azRX08&#10;4LDdYb3fYb1Z42x1jvPLM8znc4zHIw0P1Qg5e7ZnrFin+ufUuEnHsvyP2GWZoJ+stKJeRnGNS3Na&#10;IUoCuQJUOJSjMSbzGa7u7/C3/7DD44eP+OG7P+GH73/Ap4+POO724OCF3M6iNRFsbKqCpnc0znDP&#10;yqqZVuk//Jt//2S2DE2orxUSsxun8188umu4575/iWg41VE+eyYZb9w7/jXQwfm+7qAnXIwcVEgH&#10;AiCGLxh1W4MJGE3HuLi5wsPDG7z/5ivc3l1jNp+iKBRQcAC8k4RRqpG66BWlcmL+MwDn5T6tld1N&#10;s4oGuUNieoe08A8AxXPWilO+4uGgz8mb+WSJSpykxuG432PzuMby0yN2qzWaw1Gq64IQgiL6SFod&#10;Uf62HFLFPyFUms/Rq2skSo4KbkXIgaEEccQAcCAEAmpEBCftE5URDZ0YIQQ4KhAUhERPCJBU4SBC&#10;qBya2QjV+QJnF+copmO0FHHUYkxc6rPopBKXC5J/kxiIkVEVBRqtzEgNo93s8On3f8Lm+w/whwZF&#10;26KKDoWZLz2DvLRhKj1tDH2W9khemlc4F+SeWjLyz89xpziX9L1ZPMjqyDg5Tuwaxtn6ecDCtNDT&#10;1/9zt8+xPvwc7f9LjntJ9gEmf+x/2XFysEgcGu7rjk91MPC6rDsFLlxeodMLUAhWjKz0mF+c4e79&#10;Ax6++QqXd9fwkxGo8KDCgTxJJEJQMqAS/HKiZm9sKTqXdUbfiAl8ghTMNByXg28kc8mxKGAEShZO&#10;eb+uhpC13Ql2UHfXEKW4YN2CjzWa7RaH5QrNfg+0kgZgMh7j4vIc59eXUohyVIELI6T2x/Bng05G&#10;4jSkd6RMRsNyfGgxOFNGdOoEZsARyqJEjEATWlklvNOiiRrBp30U2xZHVQKXnx7x6cdPePzpE1aP&#10;Sywflzjs9+AgRTK9c5oEEHJfkr8O6DJDs3AwiqGgedp9T5Hzk42ysKbTR5zUfJ/bvuTYfJMaHho5&#10;kYEDzhdBAxx2r8E9aTBRh8cQZabrbILLpQnRMWpq4WYFLq+u8O7rd3j/9Ve4ubnCdDZDVRbistBQ&#10;UqnlIoOEiDRpjEPTNJJkxpeo21o6jZwujtbS3V+zXsD2UN/tYQAjWRCtXSkDU0QgXcSHICxm2nQC&#10;HZEBtlhoRqgj2kODTz98xH6zxWa1RX04AG0UwpDmeJDndAhRaq2AqEsBzMLGFisKi9WGG3CUjH7B&#10;3CH6ngJoSPLpa12cIInSxTISAsDWDmLis0RiFgZGMaIM0okuMupmi0PdAIca44sFqsUM41GJ1gF1&#10;7EJVUwi3doPZ7pwngDxiewQ3AUULHLYHHNdbcH1EAS/MdKenBssrg6TBka4wp1xUv9xS222/NN/h&#10;ZwMLAL3B/J/plr/CZ1scONPCs3NkQZGxrUs1bBS4gbwShUp+pxOhqPoLOo1+oCQl1MJWHBiNEL0w&#10;v1jg5u093nz1HjdvbjG7PIMbVULM1BrfIbZAZBSgVBk0RMlTQ97pgp+9fypcKARPMMNlKad7bWXt&#10;03uX4XeXPusSrVZK2enRlXu390Z2BkAIGktKpQc8gQovSbXGlYSvr9dotzu0+wN2uz3atsXhcMTZ&#10;5QXmF+eYzCegyidwkcvSRGDVd3Eqd3svkvpGeWcJgWkSMdJyEgECnzILkOA5kXOh0arSJl8zVBWJ&#10;AS9AwDuHWTnD/HyGm/sbHN8fsF/vsVmtsFwusVltsNvtsdtssdtucDwcEJoW0BBguS+ndaSgAkVZ&#10;oTglqIaWiuGcGGq0FnNLJ4fEies71+9Sa/ATNyOiJ4vhKS3AkKldlDN7LTslCmbINycDCRjJhIHr&#10;ux1IUTfpCCWS9N6OSCI5SBdND1TjErdfvcHNmzu8f/8OVzeXGE+mYjKD1gchnUgA2EkZctM0mIEQ&#10;GzW/sNxHEyvFICOSnuBtWZyMgOhcmRbPVPckWTc0I6X5z3TcRgjadeTAbYAvxBQagrhjIjQETduN&#10;Q0xJaGITsF2t8fjhEdvNBvv1HqGNCG0QgqVml5FYdFmRGYa6IQicAEmkpdEgMUrpdB28GpWqESMW&#10;OaJWi9iZQmMA4AEO6Nw8Nh446CQgVf49mBguGqFMWnYSGfVqh2ZXI2y3qC/PUV3MQdORCBzWlO4E&#10;cAwCnIBEWWrYeC+SKCzsdtj9+AHtaoMqMApITLoAINbxCbHsMMwAlqwiSSYkVSa3MNhalEtdm0+c&#10;1ODkM31m6wP6p/pcvhkbPf/eXQjZCvllG/U0U9cJzPzyZDKjv8/uZ9ak5wDSZ1ka+Jmnp758s4Wi&#10;13YJHCCRflP7DC2oWd2M9DctEpzAJbLm7Npd+jao9ihSIbeiZoDdFlQnoYe5adsqPXuVEXJslGgI&#10;x2AHzM/OcffuDd5+9Q6X97eYLObwVSnkZ1uICUCUiCeRtZReRmpcQGwCGQgXLddeqAPYXYbSrNAX&#10;bAHO8uw86bfuuhEMZ/yfHiixZRupbejJtcRywvLwIk8cgdwIviwwP5sj7I7YL5eot1vUxwafllts&#10;dw3mj1uc3VxgdjVHWZXwRSGgSrkTQX24RHlbUAIRtkCzzlkePmGmIJMT4GTnxRjBbRA5FDvZTmn8&#10;cAIuvZGc1k5CMXIoyxHmlxe4jveIbYu2bnE8HpXUusfxcEBdN4htixBahNiKZZWk3H3lHUbVCMUQ&#10;/X2JSDC3g3Vc1IX3JaWDBhM0R93PnpMa4NQw6M092edc/yjKzHFJOmXPw/3nkjognWbLHK1PQZrl&#10;zCvibuIRzIz5fI737x/w9uu3uP7qFvOLOebzOSglZuqqkXLvEfpPz6z3gAqFFFabCY50bIeMnctc&#10;Hq4AOxEY6TwDaCztEzWrZJeqRaI0mICiLBCDgANmhvMepXdCMm0alE5+b48Bx/UO608rrD494rA9&#10;CnciKIKWOkFKytSsepplL4I1b4WGjYqJQqNBNKtfzAAsMySBZ0xrZ9Cokqi/s56PKExncLc4W6Iv&#10;aX/SZ9B2RE74JJRE8I7QNAHNaotdXeO422J0dYFyMUU1HYOYUIcgBZFckYoNqUNK+DER4LrF7tMS&#10;x+Uavm1RsCaa0WOZMtOsCXzu3vtztuHsybkZ+e+ff8Wn2y9Fijx1rW5M21zIMgQ/2UxTz8dGJ0KS&#10;JenP2XR+mqY8JNQO3+Gpq8VYC/YOpORc+X7KBTyUm/23V2kweC+Z3lZirFM5TC7HKCHV5LQEuS5a&#10;6qlIeRkkByupfBMXLVUO48UU13e3ePfNV7h59wbj+RRUlYB3mgshU9ZilmeHBpyJ5+Q758CU0vtk&#10;V80PfXK9Z7s5zXMdGCcO5Gc+p1voZQzoBC1t7XwBLgtQUWAyKjE6LFCvttgv19jtj6jrBrvjHvPN&#10;DBfXF1iciXWHo3HKPJAnOWTWcHtIuzsr4ZDsHhl4ZlXMMvhsSr2ujxGk7pzMPZmhVkr37HLlWLl6&#10;p+tFdFK2gDzBlSVGkwoVppjHS4nCi7FLv67ftevgiOGJ4Ryh4P4ofnZ7DvF3yxc6belEbz63KH7O&#10;fZ5wIl5x0/SAgn4WcSUVwDqzM6dj83O880k7kgx1hiY1sTdpcqXS4Xxxhqubazw8PODh4Q3Ors8x&#10;WozhSk0HHmVB9c53z/9Kg+fv+ZKm1eNUaOl3IkpEJikTLpnoIhiRFOF6QiBZvEGSz8FDwIDwPBzY&#10;QtGC+uhaAEwYsUe7b3DY7rF9XGP9uBL3x+4oVgpNABMg7oaoY8JitQMAAuviKyACaomgqBMnamKp&#10;wN3k61YUBCN6RmhqYWnPoJ0WY2ZKtmmYTIec/QYVuDbRzEcb4ZyHIw8XIurNHm1dI9YtwmaG8eUZ&#10;itkE49KjJQvXE5IpeQcXCWgD0DSotztsPi1x3O0xJgfHAaCYLSQM04gtzyc/k4cijYk/CyZ8GVD4&#10;JUHF59zjOZ5P/lv+PQefnWXnz0QX1L/EkwyW/HSh5ww0UDopA02mpebvoPcRTb/bLYUkoRo3K3jq&#10;NFY716wOPYUtQkz51C2QyV3MnXvY5hN5iYSSkG7hRlTTMa7ub/DwzXvcv32DxfUFinGF6JCyZ+a9&#10;Y5yj3C378zZ5i8/O0PzsZV7u/9eu2Dubs/4nozsxfOFQFBP48Rij0RjFqML2cYVmu8N+vUXc7RE3&#10;ezRXe8wvLzCaz+BGJUCk0W4x8eBssZFqyCLj4guUVupEGmRImbXIa2Vm0nFjhMzBimND02RzGj+U&#10;fitI3WtskXEEOKdZpzVk1cYYNP9OWvpFxSoY/cljg+SlRS2PQui/dWbGS7ten+hDN8cTYTNoGRpc&#10;P0f/T66nz+W6bzrRTQig9y6S1U0Wly4DmxqnSP2GBeHscoG7N3d4+/AG92/vcX5xgWpUCJp3fQDk&#10;vU9WkJcKh9n2nLDtWypc79r5sS7TtgB0CzNEWAXoM5BL5dHl3XXRpohA4vIoiNT1UeOw3aPe7oWk&#10;udlhu1qjqYMwCJwIqBCihot2wKBD55xCqXp/La23HduK39aSZ0W2yA9SzgXUJdIBj6j7AXSk0Bzh&#10;QzWRbGGwZ0yaKjwigJYDEASUOQAjOMSawY87HNYHtJs9JhcLVGcLlNMRuPJKLpV5UTCBmoDjdoft&#10;conDegu0Lbwr0hiT/uIe8zrrsZNj4aWF/i9pWfhrbp9z71xGDAGG7XtBr339GZBrfIN74zMWz2H+&#10;+CfPFGHW1BxYmKIndrdOZnVVcikd11eKKI3h9JwkHCuLunIsRE1OC4gAC3aEWuXBbDHH+eUl7t7e&#10;4e7dG1zdXGM0GUuiKLIoiKfKY1ozPqdtntlMpvXbkbs15RfchmvSk+szklzIdnS2KBKQFZlABcHN&#10;xpiWHm4yQr3eICxX4O0O6+Uah0ON7WaH2fkZJudzTM7mcGUBaFi7ueQ9i9vEFKzeIw4er6/Q655k&#10;CBHXdizEdQ9Li67KnVRd7gfz2if7fwqBh7nW+gqaKLJOv3MPWIo1itASSSjqsOtem5avAYaXrBzd&#10;Szx/zjB5kG3insiAwIlzRdgAiaBkI36o9UgrSC54u6ZO9tKPwEFqToTIKatmURYYTUd4/+07vH33&#10;Bm/fP2BxPhd2tRcClXdO+6Ev8Jg7M/hr7fvcZLLnNEBh/3pbZDgj2ahmZIJB3prgo4ZjMQm6jZyy&#10;zQUGnCtQmAuFGe3hiM1yieVPn7BZrtDUNWIT0bYyPWzBD8wIHGC+QgMQiQ9hJjVzYWQ1AYz9bL+R&#10;WlAAff7AaCMrOUzcXkKn4IT8bfBL4T/XzbsseoG5G3/mFEpUVY2y8YVHGwNCU4NAqLwUqoshoOFG&#10;6qAcahzWW1SXC4yvLjCaVKIixoiKCdwE7D8tsf7pA+LxKCi+jUmLFJ9Jgnz5UzwZBwKC/zrA4j+N&#10;7V/7fXLTfLaXFIhTB0BkY3ScLuvFbl4agEjnqGXRFKWhcpdkuYEGs07qtaDzWfQutVKYfFNlzMPI&#10;jAA5htdrSN4WQouIY1uDC4dqPsXFzSXefv0VHt6/w8X1JcpxCRDQEkBe4A7HmEiaoghQ97yDFvmi&#10;1qZBzh2tF2JcA1vrP/+C+CJQ8nS96ueRyUGT6WkMAjuH1qz1pYdfzDCdjNFORzh+fESz2WF/PKL+&#10;scZ+tcVidQbcNJhcLuAnI0nCRaKUAAxEjeRydg/LxNwHy4kxka97mRiEI3WtyzpBat1lAxncWZJh&#10;ZFcCcjeyCSdGN+YYth53ymMCF3oRBsDkQb6QJFqGxuyar3XAS+b6LwUeJ90gr/3+CrgY2jGJkMrD&#10;d7tl8Uqd5DrTUBMaFI5QFFLchQpgcb7Au6/f4c27e7z56h6T+RSj6QjwQIytpHUi0bbJEiCgr00J&#10;sef1QT+0VuSTzzmX0oGfbBOypVdIkMJhsOdQc2tkOBJtJijhEd4B3ol7BELqbI5HrNcbrD+usPz0&#10;EcfdAbGNaNsW3heAF1JjzZrUyhFQlgKwAJAOZn0Zzf3PYlnQGFIhBQWEtkXTyuLsCkJsoxQZillS&#10;JhahbHHe9ug2yA2du+iFwQ6kGSG8DKSwNAF5On2VwMgUVDB5SZEd1VQZg7hJIN/bllFvD9gfDjgc&#10;DwhtwOhigWI8QlWWKNqI4+6I/cdHHJcbTNmhdA6uZZTeC9kqjQMFFkPlKR8jrwCL18Dql26nXJh/&#10;rU3mx1Md+Llnei4k9uduZk145ukQKcn+p78mt4wK+4F1NxdNJrRPLckGig1AgMz1YXPcgET3zKDu&#10;N+/MrSLEYu8FmMSo5GwG2BPG8ynObi5x+9Vb3L9/wMXtDcbzKUBahEvXNAInBcBlsr/7f18H/pwR&#10;M5TZudXZFAZd7zIQfvr8U+3H2TEvudhO/pb9TW9nz5O1edCOjgTAk1RzLT18cYZR6eEmY7TrHeLu&#10;gMOxRvjpE467A6bLOWZX55ieL1DNxmCn7+xk/RiGYg/Bp+XCyfem5sjdVmTlGZxUi44RzsBFzDKm&#10;6rUMMDhdb5LOhjwapXPDMSGBTXPiSCSfQxsIxedEfD0HCJ507wsWiycAINtcZtY5fWyHGxPPoHfb&#10;/u8mHpKbAOi7RcjMktqRdoAd7yTF9GhU4ebmHm/fvcX9u3vc3t9gfj4Xu6XjhAaZWDtULAGeis6f&#10;Bo2tjvysoB7uzZOADYFFbq0YWkf04QVX6sgg1rTT6U6q3adnFo6Jg0OoA9qmwX6/w2a9xerTIzab&#10;DepDgxAaQczeg8oCVFQ6kUqMqwKuKuF8qaQkSgLP+sFAACBuC1aQ0YaAtm3RNlKxEE3AdrlBc6wR&#10;93vEY50IRLFlKSYGQRYMJGuHkNazUZFrUyacbF/mGhHpKdYWM4LEVgi8HlZTISKEmJJmgbwwo2NE&#10;s9pi27Y4rreYnM1RLRbgwDh8/IR2tUEZo5R4huC3QtsicqbrmUrASIDj5OLFp7VDoz8SDIyeOOhn&#10;bi8J4V/6HtmeU0c92cMmQfHzE24980T9bzzYSx2TP9dyKUMOzhZFgioyyFY9VTgUYuT7ZL++k+4W&#10;sqbxcrQ/yM5DstQKNwzqggXAESQ0M8l66wmuqFBWBRbXF3j7zXvcffUW86sLlLMRUHhVkiD8LZBk&#10;aoRExUk0VniiXHL++UT7PWndAZjINwltPCHXBue/fIOnxz8HQk8pe3kZjBxoyDNB21yS+ckC7pUk&#10;z2hDBBUOfjHDeDoFLhvUyzV2j0vst3u02w2Ohz2Oqw3C5QXmF2eo5lP4ibhXg/IVHBOIHXI3FKHr&#10;9w5AZO+CLjkZVBEiPcaAbuJssQMn1zRrcUfJJC2yOabRLeukDi4iSVyYJfJyjpK3IYIA8oAbZaGo&#10;wwn0Ut89o32nDjhx/LPXeuaew8F3CtDkwKO7B+vim6Zh18gAjMTJFvOXLYa2r6xKXF5f4Ouvv8bX&#10;f/MVrq6uUI5L+MLDlV3620gBuXyQ/4kvykiGTkNNv0S9ZOaeleKlQmL5RBWioiyCtpgmshW6Bb91&#10;EBIPMxx5STbVtDhuttitd/j+hw84HhvsjwchF5JDMR6jHE1QjEcoRhLvTWUhzPHCAYWwoCOzhG5Z&#10;3w76vregwlCxIOmgSWu4jgjHBnVdoz4ccdzvsV6tsfy4xG69QXusASChb4oWjmfp06SfNdazt1Q7&#10;tnh6IC1MMO1AxolLIW0drcrIbBrlBZCDI0bRRMTNAc2+RlxtwNM1xqMxlt//iHa9w5i8xPzHiAIS&#10;OSQJZ3IXDSfriykteatZb6d+PzGfOvDWCaNfcvslrRmfBwROAw6TPf05kLkgvsAc/tq9h6DCHMid&#10;BUHa+kk+C86VG+nbHl8idl1o4MPiS0xWEFH//jZfqL9odBeCnZ0iAagQ0725d0fjERaXZ3j/7de4&#10;f/eA85tLjOZSdiB6JP+6rBmMGEW+OaJUztzunRZZbZucZvxa757i6+VW2s6il8mLL9y+ZBycPDaT&#10;6dTfndyrEpkn7x2UvO4cgbwAsTZEuLJAOS4xn01wfFyheVzjsDsg1mu0uwN2j0vMri+xuLtGdT4D&#10;eQMAZOKpayP764DkX8/6gNJuBxdzgrP+mJRu+96lXDAZHA1kBHNKaWQQOqUoRJayCgpOvPMoi0ID&#10;ChyAApG9WC6cJlsJISQzh93QGt86IMbY08pPdtag4051JoAnIaAAMrn5FGnmi5WFwZwiAqU4gSgv&#10;7r1H5KA+QwMTuvI6IBDDlQ7T6QSL8xm+/duvcXFzhbvbWyzOZoLadfI2sU1gJAV/ccwEQbaygxSH&#10;nn7/eGLwE4CqqnoWi2EbBq05cqq+BwB4nf0dqJAIdqmLYUxyEV6xZdS7PXarNdYfPmG92WJ9aEC+&#10;QDmeYjQZoxiP4EclXFkBhYerCkTnJRU2CSdC0K0IHQt1S+t33jc2WV33vorx4HW1pRkBkTGxgX6s&#10;cXmQUumxrrF6XOGw2WK9XGO72kgJ5TaAQyt5LpIW6CSyJyq3RKWF9RGREDFZU4+TClGCz8yfxr6W&#10;8R8cwBzAShr1ELcJGoAONY6bIw4A6t0OFYvVyIUWHpJIzBGST5l0rJK6c0wXTiGFaqnoqlBaG9pg&#10;ewpaCSc0sVMWri/YvvS8tCB8Dog4ce0nIZcGjJHN7dPYAxa10YHY/rNIdIUdi1zM9BYy4v7f9GCE&#10;zhXVs+pQl/rULHVKgRT5ySnEXPGGuknkWkkWEBJPylwhxgfzcBnBTo41hdIelFWWE8ncDI5Bhcdk&#10;usD9wxu8//Yb3D+8wWg21urMOn8NBOkKJeNQ5wmQss+mZqde00mvUPeFMkDVNZV+V46BhPX31xvm&#10;TGLaOTQYg7ZCxo7DZu2Z90sOGoZK6nBM98BzMjHpO0WV69mYcNZOMd9HPesVEwlnpSrhzj1GVYli&#10;OkG72qBdbbE51tiv1ti1LXbHI+bbC0zO5xhNRvCFuht0ASfnQA5iLdHvuXXYEgum7NR5E+rzMex4&#10;5fpoOxAhgRpmQG+hKQE6oFmHoPWaYqcYEcGB0cYo1lkv0Ycs2Yc52VW92NR61ddyRHlqIT+1vQQo&#10;0nf0zf/9Y5+eNwQhlqL2lImLOaIoCs3+FtFyC0aQ3BWFLvgOwnj1DtPZBDd3t/j6669x93CD8+s5&#10;qrHUlmCWtNTGyRDrQBL/2kiuz/i3+OrB8+VCPh/aKdFVFgGS3D8nzh+6RZ7eJ6ZBx0y9eiGOvJBV&#10;6waH/QG79RbrT4/YrDfgNsAVFRbXV6CyhB9VAiyqCii9hrASGjJ9H8lK0hcIWdNgsNGJfd1LAKQh&#10;pU5cMK708FWJcjrB/FyE1u17Rrs/Yr/ZYbPeYL1aSdraT5+wW29R72odX041Qso0QgnZlfLusRsz&#10;QfJ5FM5Lgq5s8kVy3WTS+gQ9C4McKBOYW0nP3jQoGXBqsSB0hLyIgGTwzLXwDJRmnfmkj7t+HwjI&#10;59oVeCJM/5Ib47QMOLl9joY5BKiv/s7Z5/4hw2iHk5qxLUg4LZ/0sXv3EKAi41dyngjSZtawwF4C&#10;P6hsIoDYbGdyP8uqqUnYDEEIWNA5p/e3/ARi6URqy5YbwHsUVYXFxRnu3z3gzbu3OL+9wnQxha9K&#10;RI2CkzlhpnJkmjqlFrBIbeq12+D9qd/WHaG+L5/zz6fc7dZK6d8Jax2pxSYaoIL0I8d+aoF0zYGl&#10;69Utf6wOBSLXmFJ3WhfZvtQ4CpzUusVa9M0XBdx4hDibollvUe92ODZH7D58xHZ/wNnmDIuLBaaL&#10;KcqyQuFI8vmEFnAORSHZOaNx0UhAjHGJrd0NAJ966zS6NerDLN4JLBQOcIUCKkbTNGgC4xBaNJEl&#10;SkbBFSDcvhAYniN8DCgR4RFROC/C1vwssW1ToZmelUCTObkTwuApWfJEf+U/cwc+h791x3c7+5YS&#10;Rb2knWeCXREZFOERSdZMkAING2ROqo2ORhUuri5w9/YNHt6/k5K08zn8iEAlS50HxGTNcRoFkvVO&#10;ptX0AVfk0CvXbW3Y+zwAE0MrxXBiTQMfdAAAIABJREFU9iwT2l+nULlc26PVAUgMFKShsE1AONY4&#10;bLdYP67xuFxidzzi2DYIBIzmE4zOzjGZL4S7oW6P1nsEEqHWKmk1PSO0/bP2iDl0/jM3BoQs5Tyo&#10;1IsGRjUqMTqf44JvUdc1tpstlqsl9ps9Pvz+T9itNliv1jgej5rJTnLpN20LDy/l4OE1X74hIUYT&#10;WrBzSZDL7pgkurGlk2ZrXJqOXQoXGD5GscSQQVEVgarV2dsJyMi4ITqe5b7PCMRniFK5O+xfc/sl&#10;+Q+vXetFYt9gzn0+f6QT1qeO7yyXJsCQBDuI0YLNd9Ytopm5xEzpPd4PAEDmf6E5Vrh38S4tl3de&#10;VnwOYlEgoGDhBwUHFKMRzm4vcffuLa7v73Bxc43ZxULqXnitE8KdVcXpuHNAGt/JM5hpDbkExonP&#10;tpolyyieBxen2rV/IaDzvWSQkERZEMNzJmTZtOnuKU+NjZeCEfL3SbxKU/GzF8zbwdrHZecZKJI2&#10;7dqBnEccATwqQPMxwmIEt52gXe9w3O7RrDc4bPfYLFeYXyywOD/D2WKBclSp9YCBFprR16UwfGcE&#10;H5hzLWvrHrqm7C8BcMr9IlGYPUDkAXKI7NCEiEPdYnuscWhq1DEod8NlCTM7EB2DybMARgT9n//2&#10;f2FjAhtpJzUlZVkqM+132DlP6pNkfX7SYoGnA+65DjdA033PW0x/zycxgMCtWjYcyDGcd2hjC4BR&#10;VAWmiznef/0e3/7tN7i9v8V4NkVZlXCeECkiUA34joToFIWKP0onN9Oz4IIpdnD/9EulpFd5nop0&#10;tWcmhf09ZT2y3yNL1AbrpPQsKai5DtguV9g8rrFdrrDe7LCra7jxCNXZDMV8Bj8bw49GyeTGzsyw&#10;TguG6WAmnwCipAAXEGPjIDjgObHxORslre+0Zu6ogDGYDXhKuvAWsWnBmwNWH1b443ff4U9/+iOW&#10;n5Zo61auwQRutbAOFYghIIaIwnsUvkLNAY26PwAa2qiQT2Bt8I7Yq+YNjpxIqNo7WlNFJz5bn8XU&#10;d8maymyZep7dJP7nmd+Yny1Mlh/zJdtfw+rxS0d9DK/52nV7iyC0l3Nwktk4Ov5Sp23D5BQROAa1&#10;inXk6+RahoGQTuO3m1q1Tkn2B5gFI/3VMgaiuEiBu4LELA2OKIsCo+kYV+/vcf/tV7h79xbTxVyI&#10;mg6SzrvTCADY/JUoEM/5OE8KOLKT0v5IfWtFAiNASgaVX+s5a/YpBUqzPyRQoT+eXiuShZaT0iau&#10;lX5xxVP3fm4T3lquQFBn0Ul/n9q88vw5Bjx8dhBrHwQSOwExw4UI7I4Ij1s0yw14X4O5hSscZtMZ&#10;rq4vcXl5icl0oqBU35Ggi7ggQ6ZudDobM8MHNCVKrWJBsyOTlwreRA6BgSYEHOoWu2ODfX1E3QZE&#10;olQVWm5JqZ+dyhzHDA9CwQVcLFBYKlciElNZa+W6YyK2GfIedkdngegPvOeWltzCMRwovc7WQUK9&#10;3+26CnisGhx1IMhEgPcOUh6EpNImR/hRgZvbK7x5eIM3b9/g8uYSF1eXqKpC44LVUsGtlN/V2xmb&#10;VpKdSLEpG2F9DJu1wAlgkbs5fEbSzIFCN7lOgLJnvud5IsRUCLQMFL6Ej0DY7bFdbbBbbrBZrrDb&#10;7lEzA2WBydkCxXwCP5/CTUaI3iM4C9dUDwCpZk2AMwKQAolc2zF+bOrnP2NLhC7u5oj9A6C4WPYE&#10;VqFcOLiyQjkeIfoCd/MZLt5c4e3je3z44Sf8+P2P+PD9T1iv1iAnri3vRdDHwFpATVxnHM1SoQBm&#10;YBCm4bNy7J6QACBKXQNYDRRO7aiqAkwTZG3j4Qw62S6vttl/vtvnWxW+/JpffAxnUIIAIgHZHZ/K&#10;hHRXhEuAglYfdg6erX6EZX1VIOK6SsBsctU5OOfB3ovF0HsJ63ZehL4v4LxWHXUOo1EJAqM9HnDc&#10;78B1jdlkjPs393h4/4CrtzeYns/hR6Xk3/HiAiZIHhoGkjVNYIMqJmTAKmsKIFu49Azqz0fA6pJ0&#10;538uPMz7u0vR7zqrdtdR3d0MnKfnp2QpgAL1U0pwd6nPGBfc/U0gq3+R3g89vo4qIJQ1RF5sERDS&#10;uIMQ9v2sRCxGKGczxEONsD+g2e2w3O2xbxusDwecn59hPp9hMploAisFT3bNaCuvEPqdcTIGapFE&#10;E0ouKCanSSE9mAh102JXH7GvGxzrgCZEtBw1h5OXNUFD9D0Jfd4zw0fARZbPDOGggUD/27/5d8wE&#10;lFUlk6UNKJ0UZ0LICsdkCDHxLwZywIhURskZmsQ6vlPX8ScRbbb/Kbjo3ALWoMmdQAAownlCExsQ&#10;OczO5ri6vcbN3TUe3r/F3ds7lFUl0R++QEh8DInyCFHQoCPLqcDpeYwLYRkgU8eZDc2o4IM2Sefq&#10;cxqX41nNip+236njhkROi2muQ0RzbHBYb7D++IjN4wr1oZFzfIFyNkO5mKKcTRFLj+CdMMYhqWkt&#10;LDe5XXigUWUauMwfS9HVTcafq3sykJKNAR2ZLW8pUpOxTejcagNmVM6JuyJqiFUTsF6u8cN33+PH&#10;73/Epx8+4PHjJzT7BuOygncFYhP0tSiFnJrpO43zhCWfarTMqo1EBjehy3bK/US+zOI2AZAKyMXh&#10;mIl9bc62biHrWy5Ouilf2P7Slou/BFD4udupZ3l5cekvm6K0d5ktDBBwWlS6MStaI6XF1apRgoQ/&#10;RL5QEqUHO+F7ucLD+UIUjqKQ78pxgvOA96DCwxVFAhditQC4qRGOe0yrEu/u7vDNuwdcXpzBVx7O&#10;y7gMHNNciRzhkuKk2gEyDd3aJ9e29Qed8QNobedZInJr38/PvXLSbWJK7fA5crmo1s2hLBStvrPQ&#10;vChnn9kYsTfHoYtx980OzKCZPVt6WtWMtO1iUhpNZjKgbmunwJWZgRBAdUDcH1Hv92iaIyJHFF4C&#10;Di7OBGRUVYHCa80YmJtLFUOtJ5M4O7AbE8xqISCRUpjysWlwOB6xr2vUISCwUwu2ktm1jUlz/5RM&#10;8AwUEfCR4dsIHwEfZTywq0D/w9//N/y4XmF+dobzi3MspjP4llEVBRxkIY0xpkqmuVk+Bws2GITp&#10;/Dq4sL+n3CbPWzU6U5f4ezqhQY7gPelDRZTjChdXl/jqm6/w1bff4PLmEsWogivVX+pVsMcWIHGd&#10;SD8xwD4BFiMKpUIvRMizRnSggtOkyDki5v4wklfP1ZQNfCICLF74BaGczHwDs5xZMFAzwvqA5eMS&#10;Hx4/YXvYI4DgRhWq2RzFdIpyOgbKEvCCWKMDCOZqYEQXdcCq35AdiF3fkgUkQhhTxvBmfLZgeW6z&#10;zKL5tfJ35WiEVaNIdpwbdow2Nlq6XmZ2wR6IQL074Lg74sc//ojf/fo3+OPv/4jmcJRaIJoX18Gl&#10;svCmjfbuT/2nCRDrhvUJRQbXLbxOSCsTb4CLucug2hUo4wRmZL/rCcRunNi+/vG/NLh4TRi/dv7Q&#10;1P3l258zgmw+9RcH26/rz5N75CTZHqC1/icjBpv2roI6y/BLlu/AO8TCSei693BlAVeUkgumLOBG&#10;I6AQPhMVXoCHcwIunIXzqVB3TlwaKh9AhBhbEAdQaOFjxNViiq/fvsXDzQ0mVQHiFp4keJCZNfeB&#10;zBuT3TZjO6tEPwInWQey9V5cFZRaVP6Rht5mJRYGSaCGWy7D8/YX9zsjtlLksNvXj7nLLb69mkq6&#10;PyAi5PG+6I/DV8EFa+BlUiq6vk+y2do0H0dsmlVnrTTzBWd9IYn5uJMZBlgVDPkoFgCoq/d43KM+&#10;7IDIGFclFosZFrMpxlWFsvQyLJKCZ+iFUn+Z9JE+1OzGEAtaEwIOTY1D3aAJLQJLEixxgUidKMuk&#10;7EAonUfB8vxVBIoAFG1UcKEuWXJgX4H+i9u/4e8//oCiGGE0GuFicYb780uczReYjMapg1OEgqaA&#10;TYOLkRZ6AhQ1dqFjOUBI4GKwX/6a+KW0UNjROo9TxzOz8BXUxMgs1f/KSmKKb++uJZPm2wdcXF1g&#10;Mp9IiWxNlsWaKMa8Y6SrmO4CcZGeg9Mgy/2VKfvBAFfIMxfKtPWW9Mp3CUgAWZBIB2haPHWhcQBc&#10;L/H7CU2BKOWLJ12IDvs9tpstjus9Dj+tcDgccaAINxnBzcTt4WdTFOMJglONJkrtFOg1bHK23IC1&#10;uBYxqQnPZ64PjXlWDSEquMhJYH8WwLDxY6a+TEOxxjKeDdjmO4t4o4jWq/UlCqnXsUPly5TPgxvG&#10;8sMj/vTd9/jj73+P77/7Ecf9XixMDPgWmsDGwDJ675cEMyMJ8Kgpy8EANUGKvgFoY4tU6YQZHKKE&#10;CgOSBXSwcaYD5jwb+Rx7bRHp9OL9lwYXuahP8tf+sh3Pwm9R1xK+wJ4lAPrkL93zmCA/eRiLsM3m&#10;HKd/PPjeXVesSABcZ3GQXAaSzwBOhK7zpWRs9WJtKEqxPPhCqmXGqkCsxKXhNGRbMqgVYoJWt4cA&#10;e0r5U1gXHh9J07S4NP6M42Dg1rFyLjhi5ICzyQS3V1e4vbrAxaTC2AsXCiGKCZtIEs9pheekjdrf&#10;BDa6e5xq/5zMnciK3LddsLkAMwWhs0p0fUwQUB9CQF3XWsq7RnOUFPsxhKTcmlWCgGQBds6hLEvJ&#10;iluWkmuhLOCrAq4U0CZ4rP+OnSWm23rghTt+VHrvdI1he/QvwrAkdpoHYvDuAi507iflAhpCnB5P&#10;wY25eiI4BIkY0WerCofZZIxxNZIMrDQIfKCOGcYqvMSrIGUaGg0rbUJAGyRHR9ApFRMQkc2h49EU&#10;DPgA+MAoDVyEiCIouADA5NG6AvRv/+G/5R8+/Ij1eoN9fYDzwNX8DH/77ltcX16lnAplWUi5a0X2&#10;HBmF98nv7ll81c470dRU8NuklDVZmfNquikU9Utly6gREwVilOgAI8R5DelJQMABReEQEVC3DYrK&#10;4+L6Anf397i+vcJX377HdD7BZDKB8x45CSYhIgxGRW+kZbicWBZfijZ6MhmpokkICeJrJYfCV5LL&#10;H6QhZ7pIocsJkcoVk05SlnZxWo67GxQd0YtZLBSehHjLbUC9O2KzWmP1uBTGcRtxYIBGI1TTMUaz&#10;Kdy4Qiy8JLtyHq0tSnx60oc8Pp8NOArIc2pSYJ1IyS2gaFmuIcQemSgyaXpg8pXFzUyIyBZX+8q2&#10;XmSXYA1GF2pbRDQrtroXHKiLQTdA1ka0xwbLxyX+8C//gt//5nf48NNPaPc1RqEQawcs1jta3jzJ&#10;7cFZv8eoYpVU02gU+Bh/Jmq11q5+ig7hzP1BJ0ZjHDTTU/Nu1m2DnV8GHoabc16SmnHs+i4bL2ZZ&#10;YjL8pwoIC7DxkDFArG62RL3XkEpl9psFIYEVMk24exbSfWBhpCe3bKY7p+fW84RQDV1ENPEZGMGE&#10;Jok1AOaacLLok3OA94iFB9Si4MsCRVmJ26KU43xZiqWT5Hwy7dk5CeVX0JAXKGSNEIEzYN6N584i&#10;ki3ajJ7VrLc4ars4UgARBbBORmPMJ2PcX8xwd77AYjLCiAglQ/KsRC0IwCyKFnltG4DIwUHDHCn0&#10;7gUYCb9rdai8sp5gHRvOebEecgAg7mXLwGx969TOEZqA404S5K2XK+z3e7RtK5aLxL+wm6UBkkCM&#10;4D+XrBeFWorK8QjlZIRqVGEyHWM8HsOXRQIVPaCWLaKsy69j6ilSgALPgW+kz1jJlCtYGGgHgE2e&#10;9UB49nZP8rbA5pstWZ2lQ5KZAYXX1O5mtbF50XnxrJvkNFYOGARgpFLvyNuAOmsWy3MWEHeHZ6Bg&#10;hg9AGdTCEiDRcWneEwIcGnIoHt68xd3tLVbrNT5+/IDVaoWCnbDnlXgo6LFbSJz3IC+hq6KpuJQg&#10;JrSNJFtKrWJEKAKcIDHvFUUHkUZEBOc7/7qZkqBCRdLakhI1Her6iCbUoIIwv5zj/uEWX//N17h/&#10;+xaz8xmqSQlfOAEWMXY5+p9XdfrdSqmZ5QwDFjaotEdSlIobpOimLsGJIVe225ByIzimwWcmOAcB&#10;YxbmI3Ww1DIDB88OHh5oIvb7AzbrNdbLDXabDQ4HKbLFZYny/AyYjFGMRnCjSgmqMpqjtmlHTus3&#10;AUPRslmqdHGJOjFiIlX0fbU2wI3NnnowQ9LQ7+6VxS0MNPYOClotELU52XWSEBfxTBoD3sV6dHwM&#10;OayBLxzKYoTL8TWmZ1Oc31zgn3/1K/zhN/+Cw8ctKq4kBhydZgZIO3gyF6FO2hB0URCrVYzCVk+m&#10;UGSCIRc4ZAtpttIQo1tdX24na/vh9vpZL29sYEr7PifrwcABVLgSAG2fQoEy68LCCkg5A6eUmyFV&#10;BENBCCXgmr+BtrvKBubsWfRtTYDmY1jkjfqNvXEcPNjLd2dp7MsSZVXBVyV8WYKKAjQagYpC+A4K&#10;HgyIwCnApG5uMAFtsu5QGqsn+yQT+tbt3Vt2B/Ui7qRTusXIFnUijRkhBGa0dYtju8Vut8V2u8PD&#10;zTUu5zNMvbkPFSSYBdfur+4IUSjkyJ4VktC5RO3hTQaqgmjWO+GsAZYcDNCcDCzRWZ4kWqs+HLBZ&#10;rrF+XGK73qI+HNG2LThkLkDKW4TSvOjNDg4IYLTk0HiRv/vNTuoklR6j8QjT2RTT2Qzj2RSjyQhV&#10;KaAqIEj6BbuX1/GZUy44G48GbKnjoyW53tsomzFPZ6NZYdL3J0d0c6ubd9aDLNVsCWgAtGr9zBw3&#10;veCe3FJnXPJuTaLsEak3/mxzYLhoxE0DFAwK5jJX8Usafc0aiCrGA0JZlLg4P8fZfIHQNAjHBvPZ&#10;TFwggFXnRmgjPn36BFd4KTFeiPm/aVrEGDAdj+CDV/kpcd4hipj1vujnijDCpJMoDCaprokQUHkn&#10;acLA8KVHWXqEGNC2NcCMYlzi/PICN29ucP9wj7uHO1xdX6KajMAOCFEjPvThyQbrZ25DYuXw3Ny/&#10;Z3/zf61WUu0U0w7hOwA+BBQqcFMYYnIlaSibAQ4icbMERjw0aA61JL5ab7Fcr7A71mg9wU1GKKdj&#10;FOMJqvkcMIa503vYwDqRIyGfG6l70gcDIx2oIpXkiaxqIXTabs6CSobuDN3cMAnIk/bvwAVYCwTp&#10;dyNBEksZ9sQSRzdfSWflc5wE0kWCQwRTxHQxw9fzv8FsPsPt9S2+/+2f8ON3P2D1uERZlhiVFULb&#10;oG0ivPMCXtCBJyapmMpEGgKd8TAy/cT6IYHNJ+3OmfT+19vMYuFI+1rlADGAKJqMYCEDRXKcVyMF&#10;ExBTNsrOxWD/OvqS1DcAK2+GXS7rAP0cWCiskQjwhEhOIht08e9ZHgThSfRQUcCXJXxZwZeFZpgV&#10;szk5ARooinQdIuNFFXCi0ahlQZn+hG6Oklnskgoy2E7BvhNH0dPx+dqp3QKnB2s3hMg4xIi2adG2&#10;DbbbPW7OF3hzdYmrxQxVMULkgBRvZRmODeTGAERo3oTsfky9R1KjdNKGTRc0AiDUaiV9KlYKRw4I&#10;jP3xiN1qK8BiucJus0VsQ3KxxJC7/rrneH1GRMSgXAzNwFwfgONOEgWWoyUmswkmsykmsymqyQhl&#10;VUpSKhLrJIeMu5VeOAOvZoXWeWr9nol5Xbi7bkxiNP+cWaRe23qAszcuTCZ2z5HEB3F2X8ouklme&#10;+k/e29NZiOU6UdMQEEQ5JYIYBSIQSOaAY7PyA+wcfFmi4CAuCSJCVZbw1QgYiSskxihJhrwHO8J6&#10;vcI//vqfQJ5wsbjE+fk5Ls4vMB1P4HyBFhEcA7wKJV9U8M4hxEz7VCRoZsRkD3Fe0VcEUURR2u8R&#10;gQPgGLPzKc4uF7i9v8X9gxQUW5wv4EqHSFFqfUAsHC9FWvwckllu1jcCkYGLHGyIABJhFJ1ZPqzW&#10;mZo7Q4SHpEkVq4BNd9Xy2CWh7SARPMftDuvHNbarNXabHZoguRj8dIpyMUExmwDjCuQLEHkQqVlf&#10;2zs+mQrPvKe8ZLeDh5qkaaEZqMqAhpzOJ4FFD+1/TpvD3EoD8iozKIqmG2ylyq6fEtrYdQbPYdE+&#10;ngjOS/ZN5xyu725xdXmN+7u3+O53f8Bvf/VrfPjxR+zrHTwJO5sTt6LTTUR7KxA4oolBCvZEglRx&#10;tbJiSgIBepPdQKSEOj4FsUMi2rBd/5yxfGqzHiKdwyz5f3v9yyponIFMe0O1SrROUuqb5Y6zsWcZ&#10;K/VBwJr91BIXm4aVYu+dRDJR4VGUpbgr7K8CBV+U8FUJpyRJV7oEIMl1lgfyhbgtwL2oD3FT2tyD&#10;5obp+sncKgkcoSM/ngYWf91tOB4iFTiEgHq9wXZ/wGa3x835GS4Xc5wtpiiLUqxLLJliQUCh2A+a&#10;2jnBf+r08G4t0naLZn1lBWUqF/REc39wZDRNje16i8ePn7D6uMRxd0Bo2uRWFFMKp7mZj7V86xHg&#10;8XQsi0wGzHIeAiO0AYfDEbvNDr58RDUZYTqfYr6YY7aYYzQZo/RaCiCyjM80dvXFnY7jyIP7ck8f&#10;6KZ2f+7+YpFTBujT3fs/2bMZSMjbh/JzeLg3A0AGHnPskf/zBDiHELoIuYIIvvBaZ6SEH09RlF58&#10;UZGtSmULR4RSLQ3eeTShBRgofAFHDuvNFpv9Dj98+hGTyQQPd2/w8OYtRq5EWUpJ6RADmFs4FEJI&#10;YvHDexXQeSMREQpzrfgIxy3gGjFlO8J0NsXVzRXeff0e9+/e4PzyHOPZGKPxGNFFtLFB4K7eRko7&#10;oILblqdEORr089DfNRTohu6hf71m/LR9+XlJOEN8VCajxHdlPjGZnME6VM2SZpIsqBRQ1TTYbbbY&#10;fFpjvVphtz+giRHsHdx8gmo+RTGbwY0l8qNFVLNxF21gdQOM1a4Pipc287F3lgprB/ndLBVWuMeD&#10;ev15qmZKuu6J9h9u+QLK/NRUm3yMzHAKxBisLqRMJFh4Fqg3ybz3mj5XgDNCRBNaEDlU0xGuH+4x&#10;v7zA5d01fv1Pv8Iff/d77Nc7AYXOa6E0h8haLVX7VEyqDA0y1ZcxgCGa+vDl7ZuZqPuaxQttmH3/&#10;JUM+WdAvWLVVIvWbK0GV1SQktVZcmk8GXhmiQRsxsiOHcYpMkhBLB1eUGi3hJIrCy36UJUjdFL4s&#10;Uhp4KuV4di6FcgrnwawWusCpT5rglD3EvXBgs0jEjnWui6hyu1hAuTV1UPAnx5rWPrQ20Iv99tL2&#10;pX04BBM58AOcuBVdCfgS2xBw+LjEh+Ual2cz3N9e4/ryHLNxhaLwiKEVoiCAiowrJC+aGdrTewtA&#10;VPmdcvRQstQJ0ZTgyYOYUR8brJdrLJdLbFZrbFYbtMdGFC39jyMj6DPI3RgWwJ2Tmk+1QW/xVIW4&#10;swxwR8YlQnQBoSHUR7H+Lj88YnY2x8XlJc7OFxiNxyDv0VJEi5iCBQBoCLAou2ndyp8H6HGFhuHn&#10;X7q9NJby9SpxGwm6wqVfuv/n91czE6fP6J0xVCMBdBmXjYjCAGsQgIMGe5CDL4Rc64sKbjRBEds2&#10;IfLSeZDziYyXqqTFCEceV1dX+K//y/8K6/0WP3z4CY+rRzyul1jM5prh0gtwcA6OS0kcQlH8mAqF&#10;hIADJeyI1vf/k/dmvbJk2X3fb+0dETme4c51bw09kE1204QssVskREmwHiwR0FewLUPwFzBkv9gy&#10;4K/gR/nd8KNtyDRACH4wPJA0JzVJyVJ3V1VXVU+s4d57zsk5I/beflh77YjMk+fW7Wa3YbADdeqe&#10;kxkZGbGHtf5r+i/nMp11CMTUgQs4SYwnY+4/esTbX3ibN958g8sH95mez/CVp0uBfdqpa887KvEa&#10;K45BE5MGjKJl5F5z99sCPdXq/GBRnwIW+vhF6FoRTK80VNF3UAioRASX8zhThHa7Zb/ds1wsuL66&#10;ZrFc0YaAG9X42Rn1fIJMJlTjhlT5nDBIKQkbWozQC8a7whS3juIBsGftQdZxGMRQsg1xMg15dBx8&#10;7+uAm4P7yeM2mEPJN+YPrOncQtjasJf76YX30PMUQiikca7ShLY2BtzY01Qj3mje5OLynMePH/LB&#10;t9/j+Y8+Iew7UhdUeDqnrsB8b9bW2OC/4q0BUDVvx9H92CnH5bccnfuq8fppAQyXQxCIeoYks0Jq&#10;SS0EEWKumsDc5SkpoM+5RuJrDTP4WoFBJoDCO5IXlQe1RyzXoampmoaqUY9E8FrO6SpfhGGUpKA5&#10;K4pSVpeF6vE4OzgA/0noE95sLZGXYvlsJo/La1iyh2K4lwaD/lMZ7/5y/Tp+3eMuxZvE0SFWXa3k&#10;iDHQ3qxYti2LzZaH98+5PJsxqT0uaQKlGoAmI6QHUpj3abD57PGNaygDUJcECZFuu2ez3nBzfcPL&#10;5y9Y3CwIbW4eWcIs2VMQUrmOhgxjmYPXARfHxqB3WiViHT4tVCIuJ9rGRGwD3U7DzPvVlt1yzeW9&#10;S6ZnZ7ix11J2l4jxkKNCyzNjGZhjgAG9qjk1kz/JPJ860sHvFm7tPQ9HX3r4J/RzeeKaSaxMXs+W&#10;wTUTlhOn9A2u8tTiqbPHoqpqnG/AV1QeUVQmghiVsfQC2jv1YsTs/rq8vOT83jkPHtxnu91ydX3F&#10;fDpT4q2gnw+SaEYj9tst7X7PeKztWEPXQUoKYkAZyrxmFlk5qW8aJrOa+4/v8eSNJzx64wkPHj1k&#10;PJ8i3hFdIhIIdBrOSX1lAvSJssPBfy2FOjisF8kpgAGDBX/CENdM71TKd6woRKtFYvYkSKkecQgu&#10;QNx3hO2esO1YXi1YLzfcbFZsY0caNfipggo3GiNNDVVF5xSpp6DehMqp0uzoicyOjyGyL+Nz4lSV&#10;H30m8kH458BSOvrckduuv16PDD53Psoq78e5T6rKAsUAW1EeR4lWaRCaOZqv/X6fs8urHkh6JY4L&#10;bUeXOpxzVKOKs/vnfHnyC9y7vOTD997nRx/9gM3NknbXEmOLdzVeUA9Gzk3QRnZZialEGtj1uo80&#10;LyRb+IPktzul0tEY/zS9FceHz7kQkol0YoJ9SoTKI+OGzkluxKTkTt7p/na5zNI1uXuu19JAI4eS&#10;ymufmBy+SNmDYWV4QawaR/cGfn+qAAAgAElEQVSIJXEqiVvsTaiSAyJFog5d9zHztyhY6z1aMR7b&#10;ZlIUqYFkDVMGIlrubMlxBgrhaL8M1yUZhPwlgMfryKpXzX3CwJEBhOzVSY4QA2Hb0X72gsVqxaN7&#10;Zzy6POdi2lA5R0pB24YfKx7pgW++yXwfaI6GoCG0GGnbjt1qy+pqweLqhuVyyWazIbRB95RVIoVO&#10;y/4HMpJkeVTmb9InSgN5YErerPRbMypg+X4IhZ9J950NkLJOeudJIbG+WdFu92xXW84u10wuZjTz&#10;sYbccvJ9jFph08PQ4fAcypfXtmJfcfy4e9y8g3pvUqrqDMbZHRX+nhOXHibxDhmXwUpocx6N09+8&#10;q6h9TZPBhZdB6NELFUAKsVji2tei7zOSUqKqvMaTu6BuShGmozGzyYTL+RlVJtaIMeqmdhX7fcdH&#10;3/8hH3/6CZeX93j65Aln8yl1M+rdKQKIombvHReX59x/co83v/iUR2884uLePepxTfKipVwiKnxy&#10;2/PKiK5S0rbZWcgo+u2RcBm815gsEWXQPFao9vnyt/QegQIbbRLTYNMMJnWwO4vyJsB+t2dzvWDx&#10;8orNYku36WhjJNYV44s5/nwG0xGp8UTnSzvmlIWts8ZcaD5BzH1RbrtM73jm4xdS7wo/zivRZ8kK&#10;8hik2Did4m8YyNzPm4VjsCNytEGyZ8YqRobeExFVXnHowcjr2BJs3XDdJAXEFsop+QECySmh2Gg+&#10;5a0vT7m4d8Fbb7/F+99+j49/+Be8fPGSmFpqVwEJSVHDLLHHCKYEU97Uzrww+YSepfN1RuZwTH42&#10;AENrQyyZVis5ErHy+LMZo3sXVPM5NBVUGspwXhMqC6tkVWWN43Oll8txbP03pux5yAlyWl8/KH3O&#10;MsgiXFLCX2SjpMpAQN93OaaUUs8JEVwPZjGPhQywm7nB8ilmPEnmA0DstUPrHTgoIyz9Ymy/y+1k&#10;3Z/2MfR63dqD+oaONboHAtJ7eVIk7QK7zTWr5YL9Zo08fsj92RjneqNnqLhvPU/e355MEJiga1tW&#10;N0turq5ZXS3Z3KzYbbZYlZTEBDGogaXuAKCvMimlp0VJ9kbC7a+/LdvL39nzYYAxJuvxo0DfVTlx&#10;N0DyPYV2DIHQBdarFZPFhLMHF8wvzxmNxzmP04DNYH7zIKm8SCWsifSegb/MHn3tPS59yNqAMAzA&#10;gt6I2TVqNKbDc/UDeq5SeB/KW4Nzgurdxlc0rqJ2NRVG/qg6SXW1o+qRjRSkboV0huQtS792ymuh&#10;BImCS47K17iUkBCpcGjttGOx3XJ9fc0nLz7m5eIlH3/2Iy6mZzx++ICnTx5zcXbGdr1iMhozO5/w&#10;9I0nvPnWWzx8+pDLx/cYzcaKoivPPuyzMjFXrW7wGNSNWecYupeKNnT4Sg5CAXYUz8PtuSmK1HuP&#10;P2rgdvy7Vb0Ua3zgogO0pTnawMvY9qwOyMdeGIa2Y7XacH31kquX12y3W0KCajSjGjWM51P8dKKg&#10;onLZcMtx+yS5dE8534Mt5hKTKevlFkx1DBbtQDiSN5qrtMOi/RiYGlzxxO89eDjl+nuVG/P49xQO&#10;IuSFZ2MYaxT6sI3dhp2jSkqVlQGMELSDZEpJKwVOAK/ymktgBDg+cyQ4YXrvnPF8SnM2Y3Jxhrz3&#10;XVbXN4R9p8vAJWIIOFGwoZu5j4TK0d8GMktGwBHKu0uwHCe1HR9/GeBhFS3e14USPVU1u9hxcXHJ&#10;vS+8Q3NxRuuFLmnPjCjqek9e4/SFJyCX7CKSlYpgqksSSLDHtlyUDCqyhC6ewbx9isLrS5nKPjTs&#10;nqQnPRssDHu6ArrNChjG5/ULjL3gaExPXK7wvwxeV/IrDtbqT3rcNYdDD+qp/DCfMojIr5HvSZIr&#10;JEqCY7fv2G47UnJEcksFp8+kieCWUD34XsvFSQqaXYR2u+fm6ornn3yqTQK3LXQZRGSkLrG/35IE&#10;mj3lxKjhkTyISrx2d+PHu9Z/+Vs0B8DmcbDDSV1AXMSJU+I7M8xSpI0tXddpC/TNhvXNiov7l8zO&#10;c0fZ1Bs1kZ7YC6wCKhVOovx1r/bwvuK4S17mdznK6jgQxQZwRBw5rlMAhc5dUeL9BwYGnI9CFXqg&#10;Kk4gs3SKOMZNpeRdmR5Bm5c5JY2UbAA7r6WoBzcFJWnAaptdHkCrZ/W5lLBwXOQbTiFqDLuuuZjO&#10;+fLbX6Bpaq6vr7lavGSxuOJm/ZJm5Li4N+fxm4956623ePT4AU+ePOL+wweaeDiqaIl0KeDRGC+i&#10;SsE7p7wCkvBSqYLvErGNVDXUvibEOFxRh5OUBY+FPPRxe2Ah+bXjuPjw6OnQM1KOsXejDkjDIrmy&#10;ISUqcdp1MEC72rBerrlZLFiu1mz2OzoSMh5Rj0bU83OkqZFRA74XCJbDPVROeWmZkamvvWJjmgel&#10;PJGocBzmk1SVO1TeZfGa8r2dCW1ll3eNWUkwjZG6rg9eG77XP1U/b3c+y6kX08DCzHNs80WmEU5Z&#10;CZV7pV8TOkY6ouYSjyYQvbr9Lp88ZH5+zhtvPuW9f/Ntvvf+B7TrHZVXN6tEKXFZSdoYLWYyOp+B&#10;TR76IqCs8mLo3jz17J8HLIbn3Dlunws+NDdKBb2GL6IXqvMzRvcvkbOZGschELuOLia6FEsn2Sa5&#10;Qg0TUSXSw4GBb3lg2JS1LIIkDR8OPVh9GSjYBbSCoy9rNniiaxc4eMRclyb2++3DQm3Hb54EGvZd&#10;qf93eI2fDNoN7uWO+fn8kKLKmx6ImaFla1sQ7X3AqKk5O7tkMp5R+QpSR4jdQXKuEc8V708EyRxF&#10;++2ezXLFzcsrXj5/yXa11vHodL2Xec9YgtRXWxSgkUHGUBbEYt6eHotjo+D4dwWKvfHQK2r09Sik&#10;zK9kvxMFcVH/DmiPjfWG3XrDxb0N0/MzRtMJ9XikMgUt0TRvSZR4sM5/msftObeVnt9HDhapqQHV&#10;39p/ROnEM9GdufzS7cspYEyZJVbHUlzPa1M3FZN6ROOqQrDoUt8htexlESrJwqy4ll0PInrkdRiH&#10;c6K8EynHqytRZkqpFNlGPCTH48v73L84Zx9bPnn+CT/85Ac47/jaX/sav/TLv8iTJ4959ORxrvwY&#10;0XaByXRCTLDbbsBpg6ouJarcTK1rOyS7kW1hehGaZlxyRkx4HFulpdrA+Cl830NkmFOh4Z1+loas&#10;DLZYi7sPVKkkde2FoAyRqsyUgKTGIV1ku1xrjffNgtVyzWK9IorgpmOasxnVdAzjEXE0Jjglx5Gy&#10;KHJmNUKwOuZCKmOTrPdowsGAo3kqTAha5QxieSWHZbWVGwiDI8vo2Fo6tfgNwAyinb0ihUHjt9PX&#10;OLD6fkwLUFergQZ9RiU2E60ySIkQLRPcQMbQ6pGD3nOFtwFANPnTTxrqpuLt2Tvcu3ePJ08e88F3&#10;3uP5x5+w63ZUXv2AMURCq3TL9Ug3YwgBy7coz20mGxRBXr4/HVfO3G0JnVJIrxPDP/Y0xSSloiuJ&#10;0EXwkxH1fIZMJ4S6JjmHi5HKd9BGUmhLvlYiDUivMgggDfZOfrYCKA7nz0Urc6UYA+aVGJ6vylM9&#10;IQW8JKjSoVtXz00FKPZBU3SNDyxN/crPU+A9iBkCi5/m8Trep5Nzm/etSxqi1hCUM3mPkMPRITId&#10;Tbl3fsGoGaNVQWqXKBeGAmE1GCISyRTiid1mrxUgz1+yuLphv9loonMmVexJQVRGGqA3EGEhEJ1X&#10;rQ2xhFJS6sE8ed6GwyCD2ZHBXOXfLdEXidkrQaELL8n10pd0W58nAxpSSlkTbRu42Ue2qw3T+YLz&#10;e5ec379kNB2XBnBa1pwU4zrdNyZrf9LjddeU2PMPvBzDNSlo1WjtPC4mYqceJctLtHs0b1ISesCQ&#10;G5RFtNcLAr6qmFYjxr4uXclJ6rVwoj/iHFEcyYKXlqdQkqKgAItiBQxdiLmJiXhXHiSFmI0ILU10&#10;zlFXNS262b/0C1/gV//Gv8O9B5d85Zd/kbfeeYvJdJxbZXteLG/457/929y7vM83vv4Nnjx8wngy&#10;pu12hH2i8hX7/Z7ae0xxeXHsdzuDpD2XQybwSQMBJVnBiGi1QFE6A4vdAIgCi8GEvWKiLabPYELN&#10;ZSgR6ALtdsd2sebqxUuurxfs2j3JeWQ6ZTyfUs+nuOlEM+Urxy4/R0Xmx3B2DwdiEdt1kqVtsaiO&#10;hU4y06FXpLaxhv8WlJ/iLWv+VcetMEdKBx6EMo6mcOTws6cE6et87+scYtc6+g6XhZiGTYasqP05&#10;ByNd9I/mC+1iR+WEi4eXTCZf5fLeBR+8+z7f/+j7XL+41jJYgVRZaWMsoMvadx+MG+bFOFQsd/3+&#10;Y4/DCcBy8loZUMRoCY2OLkX8ZEwznyKjmtZr1UjlamqpqCTgO0GictKEEHrGzbKXhmWNQ9F2NNZD&#10;lHDXs2Sg4bD4e4b/0ldg+cHHzTBLJPzgurFczwTrwbfcHdoYGH5CvzZ+FiDj4GvvAJ3DI5FKZZ7d&#10;1DALROVTpPGe8/mM6XisUiVoPx7jrQm50sKjlXjOwX7bsry+4frFFYuXN6wWS7rdvliuKYkqIhtt&#10;0z9HHorSZBFTbJrEOfRmvMprEY/lzfHYiJaZk1BPuLUnH3gxhnlXB7IwJSJmcAmpDWzaDbv1jt1m&#10;y3674/zygsnZjGpU472W/mqVpOpEDqjLf7LjtaSfgaX8p8vj4JMUb0UljrGvECJt7Ihd0jSGRPHQ&#10;IhretHCjebdsrUiWS7UXxngaHG3oskqRHuSooi3e/8qQoAlgkwUy2KhGkjQ0sEAyMjS4qx/03uNF&#10;2IUtKTqa+YiHDx7x9K1nPH3nTS7uXzA/m2lcP5eyLDdrvvlnf85/99//DzR1ze/8zr/ga1/9Gn/7&#10;b/0mX/ryF5hPpxAjTeU1pu2Edr+Hqkaco6pruv3eoBy2t3TspTQPsyZE5qEQOXRDFyxyYsEOjxBC&#10;ji8p10HXtpByxYZ3hAhhHwgbZYdbXd2wWq5o245OBJnN8NMx9XyGn4y17XkhFNN2trY0o/RJVhqb&#10;zJm7A+tpCIoKIEgmZFJeeFLoxZ11Ecx/i/S5KCUmarN8JNDgdsz3eCP5o6qaW9c5ch7eusZR3sjx&#10;8TpCXJXFMfgyweK1edxAwBVAlA7v1b5ryFARCdSjGomJ7X6PGzme/cLbnN2/5MHTx3zw7nf57JPn&#10;LK9vdH2KI7aR1Gk/nj7hdZi6Bq/xWLfH4hVAZHjO8e+nlJOIgiytMtVmdTELgvFsSj2bEmpP69R1&#10;7RLU3uGo8A4kBtok7EIGbYP2AE7dBlj+FjCIAffgOJETMjPnxJ3Pnd/ywxeTWbmiIZrjj9uzF6B4&#10;ePT6WA6+4+T5+bXI4VJNx+f9JY67AOCrcnEKnbzJtqGhlFJWFpH5bMKDywsmTY0n4SQRQpsT7bOB&#10;iCOFxG63YbvesLxacPXiJeubJd2+haAeDaJ6A21NWZ7TrZ9s7SqQMO+h/Z3KMxi4eB0FfXJtA0mC&#10;hjgGoMI5S+wVcJo/0Bs9ccAwKuSseX3PqTd7fbNiv92zuLrh/N5FT43gnTLIppRzVGwl/wwP27+D&#10;/S7onnKQkywTtTgayQT+CUIMhfZAjWFdsX3IUL3jqZQ5ankyCD4kaDvEV3gG4DyPexTVTck8FyUc&#10;ko++R0TKLrQcFrBJFAUVIfVNzEQcyalFFiURXcd41vDgjce8+YW3efr2My4f3ldeBgJVXatlI5Bi&#10;oHKO+WzGl7/0Nt/+1rf5wz/4hG/9m/f4/d/9I37j1/9d/pN//I8ZzWdayho0Mcxi1+bmDgJV5Wm7&#10;Tu89A51TLc8R9QAcZyZYPHZITz2cwKJc86aPeYIbV+kUxUhsI5vFOhPGLNis17T7johQTcaMzub4&#10;2ZTYVFBXdN7ItHJ6aIQGZS0MuXQ1ZkE5kBNlIQmU6GQJNqT+HEQKOKwGnoqD5zv82EkFZX+/jut9&#10;yLJ37HLXpLHbAmF4T7fbJh193yveM2EqirpOnm9dfY1cK8aYK0oGYzD4UF4WAzeipjs75/CjGrpA&#10;EsfZw0u+NB/z8MljvvfB93j/3Xf57OPn7NtOS67FKf9+1pspUZTBQfiDV4OGY2BwF6g49fergEi5&#10;rg6eAipJiHdM5lP8pKGVRKy0XLQLEZdDo+I1PKgWo6frOrp9S+qsJ6ytXylW0xBc2BinvN7jgVIc&#10;zMPw76PXbZKi6NyeXiiDMT5+67b74m4gMric/S5y+7yf1nEXWDx9jpTIBMV4SipcUqSuPfcuz7k4&#10;m1KJhlCq3C9FUGuXJMSgsuz5p59x9fw5u9WWsGs1QTAkQmd8Mn24K4Rw2wsxABelcoP+nDiw9G1d&#10;GvD4ScamD6lkqzoTNBINaDhiEmSQl5Gy16L8LlKeS41SiLFlt+/Yr7dsV2vWixXnDy6ZX54zmU+V&#10;siFRCMB+Vocahb2BLFDyHUrWg6ixWzwyomI3JpQEMIP8oVcrZe+FlXOk/KJFNYhK7NjlTr/qNNQx&#10;1kpOVyrAUkrquRhOTA82RG9goMwMFGv8WZBMAxpzfwDnHfW05uGT+zx++og333mLe4/u00zHWtvu&#10;IEUhphaX62K7rqORml/9ytf4L//Jf8a7773HH/3Jn/LNb36Tjz78iK/+4jvMJlO6XUvX7ZnPZnTd&#10;nlEzYrPf0Ywqdvu9ZkA7Dx5lUTQvilnpRWhSqH6BvrpgOMB3uBztlcp7Qhdy5rGWNnVdx2a9Zrvc&#10;snpxw3K5YrXfkipPdTalmY5hMiY1Y0JTawOlZA3BjNEOYukrou53SEg4nBvzVAwzF0oyWr5Tl117&#10;ksGFd07pxl0vCOyBj92/dwkv9YwIbQz9Ij/ybAyTQ+8CIuLcwXvHiZ2v9FoMhMfJezzxeZ3PQwsZ&#10;MZKlvD4sPJJS7oeTv2/guTDVkZKQukAnsQekLmXSp5r7Tx8ymc84uzznvW99hx9+70fs15viQpcw&#10;8AKa3Ddgk7KSGoKNO9bjrXG5A2C87rm9WziXpicdE19VjOcTpKnY0dG6Spk3c9JqylwHKXsFR0Fo&#10;cHTJEWiJwUiR0iGIyNrYSSwL2NbYABuir5x4jqM5KnwiIrcSQvU8Kc9ZrpfK/yg3YOyQJ7701mvp&#10;8NdXj/yPf3zeXN72DppZ2NukNujq3o7MJlMuz+eMak3ilDy+PiPeFCLr5Yar5y9ZvMyNxdZbUhtI&#10;XRzwU+h4JTJhVZIiP2OKfY5FARcx64oh+Ig9/0ieiwHriz2kPWD/zIPXygjZ7wZKyfk0eX3F4sXI&#10;pc2mE/KPVY8gDpzXZpponhQhEz56hxdPt9nzcv8Zm82a3XbL5cMHTOczZZJVkqO/FNBMWTbcMoyS&#10;AfQs09C5KOAiv2jloLrP1KBLXo0AzINkYy427phC1E2dx0oNgKQ8ViFkjhIhJNDCODUqtOGheaWU&#10;Uv7w5uk9F06qPFm6YR3oTVaS2TcDMUV8JVycX/D06Rs8efaYJ28+ZHYxYzwbK5ohkSSUBxEkx/gc&#10;PtNN1aMJZ+98kTffeMrX//rf4Ps//CHvvfseb7/5jLqq2IeOpmnYt3sAvveDH/Dp88/46le/xmQ2&#10;o0uRLgTEV1RNlUvhpCA8Q2GmWSwk4kgFX1kcXp/+DoGeXYBOHHihaztWqyU3Nzcsbhbs1jviLiK1&#10;Z3x+DpMGmU+RcUOqa5LztDFqc8WYwxs5iUZDGYKte49Tt28aZF3n+YhQOice82lIRqnOebwTPJqk&#10;5LJzX8zfkdQKz5OSuTKM65/DcEVuFS3iMAbEspAtkTVniId9S+g62jbQtjtCUMXicuwW5xFJeK+N&#10;g6qqpqoc3tfgUIr4WxZjwspwP3fTFgtUd6gYmmCgWBNK2iYgKMBIublLF49KYe3HFLLzdKEjiVKJ&#10;hxRpc06FVJ4UYXw24Z1f+AIPHjzg+x99n/e/8x6f/ugTwq7VxKecuZ0sNyeDeQvTnYynv9KSS2W0&#10;ipL5HMvv9nfoeGkTIuWkCAKuqWmmE1xTKaiNoVBwa/ImqjQk08IltX6ruiE4T7vfayttA5GDRGQM&#10;6MuwwsGVu+nd/IMeMnl6zdK+9QynhPLBbwNjYgAJJJk7uJhSZWQPjtTfw7Gz42cBMO467sqZKbl2&#10;pB78QvmZjkfMx2MaURBYkyBorsx+u+Pm5YKXn12xvFrQ7nYqqzsNgTiUOyJmnorSu2hgICi3RCjJ&#10;jb2Hovdq6KQfvl+UnmRg9GN6Lkro/sTvxz8lifPohyjKIJ1QGvnsxfA5pJJC0lYTXohR2C7WfLpr&#10;uX55zdnFBWf3Lphfnik/05EMMzmUgxCHc3n8THcA6nK27Yss7zneC5akn/q95EWpIlKu/jhMFipQ&#10;HVvFYnuTHNISl3VUPkORi/a7y8BRukiqIfqKypBJ70pGa1fzCBtzZqme8IBEnEvUTcXZ+RlPnz3h&#10;rXfe5smTR0znU6pRBV6vF+1GhwowP0AsQjwv1hSovHB5Mefy4it85ctfwDtPDG0/5KIt1/+n//l/&#10;4Xd//w/5pV/6Bb7+jb/Jr33913j8xht4L4S4V+s4Rirv6PatJnGiXg0Thl5g37Y0zjNqGsKuLXHK&#10;QLbKktUwk5Wq+mx3ux2b1YblYsHyesl2vVH3XuXx986RyZhmomyasXKle1zK15CoY2BiNhrzUkbR&#10;2vxMlHzGpTJ+puA1ma2nuTbA5Jw2oFOl6rLrKyPQZBZCzBVBGX2SES2CUUzoXpcCvFLqF5/LGiF2&#10;ytWx22y17ft6R+gCcd8WwBFCp58VCzfpIjXadyfgvDYFq+oGX3vG8wl1U1GPRlRNlXMwtNhXk49i&#10;CXPFEHLjPadxf9FkNMhgSga8EimpMrewV1FsOdSVx1lqRyDqdULsT8IEmXKdRBmWz0ohY9D7E6pR&#10;zcWj+8wu5jx6+oTvvvs+3//gI64+e0ncB0i55CtqqTIIXciKO9+bCmsDHLnHR3Yti+3PLCDE6jLS&#10;YZdaBRq2evq1cuvIVotDATRNTXDQnM9pzs8gCVVyNFEVmMtWa7IxNqUhQiL3nfFeW5eHQLdvNdkz&#10;hCyk+jCMy+C6WGzD+1N/LdnRUW61QIMTVt6tp0vQq5t0dJ3DE487B8vh2+ThPvz79mj+TI+78pkM&#10;ZKqcE7qoZfu2XkbOMxZHk2CUHFWKSIhsN1tevHjO1ctrVjcr2s2O2ClAUK6E/MyJwnh6/BMt9JGU&#10;t0LDHbEPiRgAoc+z6A2YlPHGYZjk+DBgcPz8B+tdhnv1NrgABQ7D14bJ7coJ0hNLJgsTmXERgSBF&#10;x8Q20G1b2uWWxfMrphdzzu5fMjubMZ5Mcriwt/qso260cYCB7Lcp7LlvDuQXqeQx2ek92MjnSlZi&#10;5KaSISGddi1GRPOZzKgsuUkD2WBer2TrTLJHRw3J2HU4X+OTet/DPrBarVhvtriqYnz/Anc5pfL5&#10;Jocoz+dM0SiZMMoLiUDO7sT5xMMnD3nznTd5+uYbPHz8kLPzOc47tfpK3MVu9eQyObJKTXsm7azq&#10;helkRCLH8UTdZSLCcrXmW+++z5/9q2/z/gcf8bt/+Ke8884zfu3Xfo3f+q2/zy/94hfpOiXeartO&#10;y1zbrrhrBsY6k9EIFxPddqfUtNkSkzywkjSk4JKj6wLtbs96tWa5WHJzvWC73RKTUPmK0XQM0xGc&#10;z0mZotu4LnSDkXs1HKyHg6RSW1BlXCwJaYhk82IwG8zb5sgVMV5660zXbDoccStrMbCRG54pbV1C&#10;nUoO712pqHAp4Zw2rksxsVmuWLxcsFos2K02mlAbkir0pFa5lZZp/XcqVo4lTkUpW0Pv2ytF9HLk&#10;kErwzYjxdMRkNmM0HVM1DZI5OIbJa95VCg4ItG2Lr+qDjVKEjkvl1WTCzOYiX0sEtVKyd0bXy4Dk&#10;Z7ANh9PlbEHFPnNcFW+kno55+vYzpmdznr71jG//P9/m4+//gJvnV1RRGIl6QnzUvJggOQ9HKC78&#10;mEv1Qu6VoJs+FPAn2YQRbidaHVolpyzC3sQSlFQoirAHdgLNZEIaNSRR0pw6Cj0jiW6WJLF4dihz&#10;K8V76G19dh3SdtqiPsSiWBQ8CF5XYRFqZimZJVpk7/EsHDobXpELerfn6xhUnDr+vwYRp467gQWo&#10;UUGelr4NgQM8QpPZlF0bYNey22xY3iy4un7Ji5dX7LY7UqteisJVEhmUHh56Gg49E0aclQbgon+v&#10;v8Pb1SHD57orUdye9/j1oRcin3Rw/klwccdrQFnH2pNE+weph/EQkBjHg5JVBbqQ6Hat9lS5umF2&#10;Puf88pLz8zMmswlVUxNDfjZJOCdg3VhNBxvoBszotkwKl5PvDVQ4dG/1BmQuuScbwkk7/LqYlAY3&#10;mkFgSZfGFQOlL1UOT8pAJ5l3T1BdLNJR44ldYrPZsFpu2W5aui7i6obOeSaTiqoSny2NfpLwaPYv&#10;AedV0JdeCxdnvPn2Ux49ecTjZ485O59n61LdRckdTu6PfYgK9ohAtGSwPpbkPNx/cJ//6B/9B/zK&#10;r/4q3/zmv+Rf/at/w//9ez/i47/4lL/2ta/y17/yFZbbdW7NLLQJUuVxrmbftlTO41Ki9p79bkft&#10;PbsQkLqiDYkYOlWkoPkObSTsO7Y3S24WC24WS9a7HdsQoK6opzP8fIKMR1BXUNfgHJ1tFJvAI+F/&#10;+kgDc6qPqxVwaS59UfediOjzOFeyoeVYfKZeiQ++RWvAc2t2SwotxGKiPB2ld0tMhN2eXduyul6w&#10;WixZXS/Zb3dIrp1WjwaF/c1L3ni97srJUqY4zQpSxRm7jtRC2AT1IriKzciznkwYzadMplP8uGE2&#10;n2lXzdwPhAH61/uX4WOXxFtbl8fCSQZ/229O1LMS80YMhCLwhL5M0eam/JutE3xexynQhhbnHLOL&#10;GdPZhMl0ygcXZ3z07gesrq5J266UVhaXRcg+P1PMZLa9ZKDy0CAYqr0hnJfsLes9UCdCLkcPkwTE&#10;edoUYDSmuZgjI6Xhj9mLUJraGVSTwbKV4ZrLgt8rAZ6vKrzvCF1HaDud8xhLiEPnKSPvaIJWbj3z&#10;AJrnOexH4C/HMvBX4VmR5WoAACAASURBVJACAhQsqsO5EkftKyqE/WbLy+WKzc0Ni+sblssFm81W&#10;923SKhHzNhSQEM2LR++NOKoMiTEWMH6qakSPw4TOu8DFqeNVr5eQRxywk3IaYAwTyO2+jXBvGDop&#10;hImnvBxkThhJypGRK05IkW0M7LZbVtdLbs5mnF2cMz8/YzqdUI8aragT3dKkPoVSxFpwANnz3N8n&#10;CuRLflJP463rX727LstYl8Pt6n3q9YZ3XnV16sPtBztGB4+D3ZZSdiDGnKwdWa92LBZrdtuOFDSR&#10;W7pEd+0Jo5rKLiyZWRKJBFpcY6WbwmjScH7vgjffesazt55x78Elo8mIalQRCXRJSzE1SVMME/zk&#10;h7m+SDrAVj7qFKnVTc3Xv/51vvYrX+Mf/sN/wLe+9R1+//f/gM12w5feeQdCYlKNaEl0US2SXbvn&#10;/PySphmzW22ovGez3DIeKwumH0ELJOeQ5BXh7bUJz265ZrvaaFnpekt0IHXD9GyOn09hMiI2FW3l&#10;dTADuBBLL4vjpMfPf/wcE8wmdflE/mxJUnUJL1n5DNA0w+84UiQlDph1mOXExHDs3lfBIVlIhP2e&#10;xdWS1c2CzWpN2O7p2hYfE41ozoRLmrMxakZKquI9VeVxOcG23JKNiUixdjTLXBfjrt0rWEWTqdqb&#10;Jbv1mnVVIaOa1XzGeDplej6nGY3wPpO9mOJNQ9RtHjQOvr8PJ/WvDecokS0+ceD1fu1eS+Iqfdw/&#10;73ssl8fImsjEciF7c6RyPHr6hNlsytOnz/jBB9/jBx98yPXzl+y7gEtBnZnZlBHpp9OJlhHH2LMX&#10;Sk7asrp+Va3u4Il06G9XmZx8esmj5hxdCtTTCZPLc2Jd0YojSCxjrNaTHkGMnj4LNW5b2Mbm65zD&#10;V1p+3YnOcU+ilEsESTnhNoO5PJGGW4wz5NiP9PMAMD4vyVNBagbNWSYYS3Ajjna74/l6TVyt2C+X&#10;hK7LoEDXbDBL9wg0GAgvYYsj8DEEIQYugIP31ANlzckOgcStxO583DIG7nj+cv1k4bK78yuOr3Xq&#10;vJTSIcnW57yu8leTRyUI0Tu2bWC32XD14iXT6ZT5+Zz52ZzJdEozHlGPG+qsN4wAMqac+CrZWMz8&#10;Ub1jRoqhipmS9l6+J4dT485cInkMtXKG3sNU5OChAWry81B3kT0pkXa/Z7lcslptIHoq12Q9EujW&#10;W3afOSrnbWD0+tFBl1qSd4ynEx49esg7X3yHp8+ecnnvgtFkrKvXJ80FyDessZucJZxjVHcdQ9fW&#10;7YVj/9PDZ34KX1UqSNFSn9pXXF6eM5tNeeutt/mN3/ibLBZL3nrzTbZxj6sdbbvHJc/6ZsE/+2f/&#10;LffvPeDv/b2/x5e++EXGkymjasxyvcQloRpN2Ow2uJgY4dmtNqWcdLVcsdu1OhFNzXg+w08muOmY&#10;2NR0PpfP5RJHJ73FNpwg22B39TjpZzEqii1jIwXRigh15bPlr/XoZOXRVzYMFsTAj9Fbl65v1hV0&#10;QackGsNHyZMqhC4mtquVeiluFqyul+xWKyq0u+G8qplUNeNmwmRU09RjqsplMh4Kp8iws2o/0dnp&#10;kIWRhY5EtMNoSIG2C7RBQx370LHvtEXyertj1yzYLhbMz8+U+3/c4ET7XUiKfc5CnouU0bgBAauu&#10;MUWJ/ZtPsHs1ITKMyRoQSkfgr59v7UCZUm5LnPeDfWdKML08Yzqbc3F5zvxizkff/YBPP/6U9fWC&#10;katxueuPz2XJXWsVOr1QGSa4mjcl65MjS+RQcNw6htOS8n7WFUg9mVBPJ+xSZE9AqpoUJTP49U4G&#10;KdbO8CuLNFSDQez+HT5n5bvKE9qgJW5tSxcjLnSaaGvPl7tkkj/fU1icBhY/70fKYMLGysWERzlJ&#10;avFsVxu26xVsttDu1fOUPUWQLf9wAhjki58CEQfhj6NwiV2n/Nj3DbwXds7tZ7n93i1ZMjhitCqH&#10;ASofXCsN3lNDIO8n82zaa0cA5BiYDGXCwQ+S66hz+LfySEh0+46bzY7V9Q0vmobJdMzsbK7ejNmM&#10;0aihqmucdyRfEdxgXAZ9V0rbifxoloBsBpvkIok6c8tYk0kLP5p+LTr/hFBIxX2ZZVoxzszaUdqH&#10;8WTMdtuy3exJSfA+lyPvhJB2VOIz3asJVAez8xlPnj3i6dNnPH78mAcPHzC1luelYUvsXVuknKBI&#10;flCT0q9/HEyms+GilJMq8VPmqvCOtm2JnQ6Y98J8PuX8fK4Nkrxjs9sxGY3Zb3b8yR/8Cf/8f/wd&#10;mrrm//jf/i+++rVf5Bvf+Abf+PVf5/LyAocjbDvq6Gm3Oz57+YLl9ZLFaknbdUq4VNc0kzH1dEY1&#10;HWk+RaWWV7TFmg2pPhn2cGO8alOUZ4e+KmEAKqxLqYYaDJ0eXs8EwNBHcAwsdJGKrSJSUvr0ynpe&#10;xEjad6x3OxY3NyyvbtguV4S2o0rCRTPh4uycadMwHU+pK4eTigo0hCaCZFZKkcO1a2v5kIQLJBfd&#10;DpVmEVYpEZImh+5DRxsCu65l17Xsdx2b6wWpC4xz2GRcV4XQxgD7gbdmcC8GDIq1m3/xksl0jtbn&#10;LVCcOy4aHX1+YzDq+n0hZc+QkGlydcO7CuYPLvjy5Je4/+QBH333Qz567wOWn13R7jq9v5zn4nJd&#10;uYLXXP6X1aw1dNfxPaV2y0N8rufMHBcg1M2I2fyM0WTMJhMFaRMjCi+AgVW9m8yvkK9jTa/MS2VC&#10;3CqyXOVzeCtmoSrQtqS2A4vLSy9YwUj7/ir7JX7y45ahhvV8QMmUXIXEpFTduxafqyGISfNfckVA&#10;CvFWSOOU5yKGeCsscpyLYfc1TNIs/xbg+PrAwv7+PO+FuFzIP/BmFMAhltN0O1xSZHQOkZzyThxf&#10;cxgqkZT5lcRpBV2MOSSoHYJpA227pdvs2CxW3Dx/yWg8YjKdMptpAmg9HSEjBRpuSKXAwBCC3itV&#10;DFn924sahz7l0nKjJB+AjLJCxEKOvXAUOQysi9w2Hpz3TKcTQpcgLdntAqHdau2RE+K2pQp0uEoY&#10;j8fMZnPO7p/z9lfe4tGTh1zeu0fTNIhIJgZJuUwnKRFQvrmCnrK1oUr/9Y7bcTHwlWXz94mK5ulN&#10;KRK6QF17uk5Lm7xzec0oMosxaDljCjSjhoePH/J3/73f5Jt/9uf80Z/9a/71u+/zv//eH/ONr/8e&#10;//U//a+oENY3C+Ku5flnn7FYb9h3LZ2DajyimY1xozFu3EDTsHdCEl+Up+CoESWWiRGcWc7p5OI8&#10;1aBreDjpPUIKKHoPgBF4Fat7uKnK4rtr4x2iblVSOV8iCrvtnt1mxerFNavVgs1yBTExrUecXZ4x&#10;H0+Y1iNqX1F7R+UqrCZQnFWDxNxkLR54BIor24EzWoOYtOx26FWQRLvfZ1Cp8+qCgp9Rpa29uxhp&#10;Y6BNgS5FWhJxs6dFcOMxvvLZe6RKMDkjFepDFra+4FARn/IpDZk1RNAE5wSSXF5vMVt/GjuNOanZ&#10;GYurZMde7tbYth11VeGcJ3WBkZvwxuwZs4szHj5+yAf/9n2uPn3OzdU13b7DS/aAmFciWTmtehd0&#10;Fu3+Uy+Bjp7i7pcG7yW0HA9hMpowbkY4HJX3iBO6qGGLlCnpfQbVZgxYea3lXkl2o/SgVkrISJz2&#10;JZDM8NN4R6or4m5PDJ3+mGfk4LOHgvbYSfrzCD0OFHB2j5kl7RJUTvAp0W73dOs1abvFdx1VSroP&#10;M9BXHopUDA+r/Cgt0YdAIlqVX7r93ivAhb2mFrIB4jR8mN4yP/BYDN/uS8x7J0U/85Y7YdcYGga3&#10;dc5hPkUCJISSID/0Zgy9HmWMRb09pZVEjL33Q0SbwYlxIuXXvdCFSNi17FZb1ldLbpoGX1dU4xH1&#10;dEQzHjGeTGhGDXVTU1VVNrTJ92qklz2FlktaNquBhaTVbpaQnpKyEtvvokm+/RDnZ7GQCCqj4Vgi&#10;qkNCnGM+n1FXNYvFmuViRYitwtkIlR97zs/PePbsGW+/8zYP3njI7HJK1VSFIwKxcsxArlnsJzMv&#10;YF1sSo182OrrczYClEm1gfe+/14GwslQrtZLZ6tNlEmucV55Llz2ZgiFtOWrv/o1/sl//p/y3nc/&#10;4E+++ef88R/9Me995zuM6gYJiasXz7n6+FN2qw1tTITKU09nTKcTZNrAqFGK7srROQ0XOJeT2mJE&#10;QhpQrmovBpyCA3veV7n+yjiYZer6ChAt1+xdbub+ssk/2Hy2KbMSPdqyRSjHENQDgiN1iRQC2+2e&#10;6xcvefniM7rNFmJgNh5zeXbBxWzOyFVU2cPhbW6yAJLMwx/RWnnL6c0euDKV9owpc2yot0eO5Eqi&#10;qiv93cJIAuK1vJGYqBG8rxhJRXRCmyJdSlpmsNeqC5wqOGU3VTroGNNJL8axbiX2I2fK8UBdiW1I&#10;FWxRcgJ0pjX2uZwtJR1rBteIKSJeCDFAFpDJJzyO+f1zxtMRjx894aP3P+Tdf/ttnn/6GbHVSi1t&#10;zKeNAs0qVSGdmw2mlAHUccqwKuYh0LWXezHSn+lyF1TvHGG3Z79Y40c1zbg+AFkZIqB1iuo3Ebtu&#10;2bdaSmvrU3IiWzLIJnn/ox2JXaqhrul2W7a7ROxs/FSYx6R73lg9D5ePwC0x+PN3mAda95gCXyee&#10;FCKb5Yp2scS1Oxqb9+yBIIedh+DCYvMH3oiBN8M8Ffox82wcVodAf/7wOAiZDNLch3ljQ0BwLDqz&#10;E+L26xx+tozJ4PdbHt+hdyBb7ppTNfBYlLF1B+fe8migYMR7j4VOnbcu2gnrUluuFxKdBGKrnZPT&#10;cg1ecFVF1dTUdU0zbhiPR4xGI0bjEc2opso5bVWlnhFxOdfCC77KvE3Fu6SgwsJWQ+94v2cycDcg&#10;lk7ran0t91+pHDIZF6ywWm0IXavcSv/NP/0v0rM33+DJG084vzinGue4z8C9OsCbeaPHgjjNPaNf&#10;Z66ufNsiA7HVT3zKStRi8oWeO1/DVb6snJTSrcTIGCOVN+tUB7BY8ua2JUGmeU3m5o6wWa35+Pt/&#10;wbvf/jbTZsIX3nqb3XrDarHAOc9oOuOj559xtV7yS1/7FS4f3acVkLpm3W6pmoaulAdRyJCccWAQ&#10;CX7gJh8scntC2xAGlvTZtAeF5lRk4SlgSXDIIJfCLlCU4GGMsXSMtblJNpsuK6SIT0IKkf16y+Lq&#10;hsXLK7brDalrmU+mzCYjzs/OmY7G1N5pjbTF2aVfkJZbkiST3qaY2e8YLN7D+bfXki2Eg/cBlxuJ&#10;JVVILgNaW4zRPi8QnSiHiK3YzKKXUIARM+lbcup2T94RckvvW3nHGQBYQqIwsLrLPPU8GuWJkhJv&#10;pRAzLf5AcIr9SL73Q1V4YIUnJbDy0bO6WfHpjz7mex9+yI8++iE3L14S9x21q3CBwrmSEiXB01lO&#10;UgrKyldu8dAfcxCuwELtQ+CqSTNx0pDOZvj7F9SP7uHvXeBmE6IRaIlYrW628PQZMuHI4dC6PtRV&#10;BPvQurbzErio7Ldt17Ldd+zbtgetgMPn/Ua+/xyaFStTtuDM6eMV6WB/JY6UkvKLBCU1GjvPzHvc&#10;vmXxySeE5YKGSI2yKPZYNK+TQbJfAQ/pNriw70rx6LWBpUyW9wa803Be0lDLDF+OB9ez465QyPHr&#10;QwBs8ja/cuAh7987Colkq+jY6D0FKE41f8ww4qCH1UGTTBleQ/OPhtfIZYC5gEFKAnRVV1RV1QMK&#10;76iqirquqKuauvLU3jMfj7k4O6OuKuUByjlisWshxsIpo09OAZTDoHofPul/hpIr8z5nAKJCbrfb&#10;s1xuWC/37HYd1Td+8xvMZhMtJxVNlhwqhENb4FDSFiURTUnI0WSmk1vc+8M251b9cLRiyr/Hb1mS&#10;XTRWxGQlVwogLJYXScoGCaS2I+wDVRe5P5vz5WdvsVysefnypZbHnc8Yz2cEcfyvv/1/8i+/+ae8&#10;+cUv8O//1t/nb/+dv8uua2mqRnkGDCZkEJUEguTcSMnPferBDWnHHjXafZexEMmegaNNU67NwcIo&#10;Lx65HG3QTGF462uRSV8WiwXLl9dsV2u6zR4HXIymzC5GzKZjmqZh3DSqaG1BFgCTBiyLkESFRsz3&#10;nGK/gm4PwyCed4ewCPn6Wc31QiYvMzfwCBlxVVmaIaklJpByy+UUHclFYqXxZO/7DT60fBP0JY15&#10;vAvAGIyvO7jv/loRdfOHrsvGXCpEZCkXc7i8oYoHJYOokuwpGsaZXJzx5mzMxcP7PHz8iA/f+4DP&#10;fvgxu/UG5xzdvoMYlJNGpIxJDCGD0uOxlVtArn9rKFgVgCXvlYtk39JdLei6QLXeIvMpk8t7yjjr&#10;PTHXFRupVUxJ6+rd0MqhAInj7y3LqthQCk6cc1R1xagOSNvStnul3A+xhIVCce+6bEiAQs+fx8BI&#10;f5iXwFiKvVOP32a1YrVYILtt4SKKA6PFuk1YJ8wCBrMyGuZhHIKJQ/Bh5w89tvkL7H+f68k9DqEM&#10;/70FJo48FH0YZCCy6MVNLx/TgX6x11WmHV7rrp/j71SZ0nsVDWAc529Azr9yiRDVC5CilrK6mA6S&#10;4MVlw6UN4DuCQCvKx2N8RJXT3ETvhDCfM06Cm05VLsfY6xN77gM93quXOyH5UHwU4BHLp5x3jCcj&#10;vK+YjFrW6y3V5cMLkvIRoiQcSpIkA3h/a6MeQX9LBrHJES9lYQ0XhApBoarrQ7Q3mPnhlRM2IEfP&#10;aYslbwsRi/XmmLeDJJkFs1MGurDecfP8isXVDbvtjrYLiPcwHsFsTDWbILMxm9WGH1095weffMzz&#10;6yt+42//JuPxiNWLFXU9UuvSUxYgZE9BuWP6hky2auMgHyJvTHt+bxUVllOB5Ek7TAwcKmLJbqm7&#10;kLzLCZJmQVRVRY1jt9+yXq1YvrwqXBU+wXw04Xw+52x+wXQ8ospsbC5pCEqfRb0yiXSokIWSdJcG&#10;q/MnFe8puw0zbisJgqQeVzvRRW0Wr9ADNvt2JLtn83vBkVn1BKmrEjYx0o0yhzK05HRNmR/DNuqx&#10;W5WUmwR5vUibBbGW9+o9SMxCiLuTKq1KI2Z6d8Rz8eiS2dmUi/v3+OBb7/K9Dz7i5vkVrhYq8UjU&#10;NtceDTloCswhILeRK8uxICfbcwNg4YSuUi+Pc5mefttCWBG2HfFqTbVN1JdzqvkUxjVBhG7gOZKs&#10;sESkJ8RKlmA7EMJDwCF2JwAuC3fNSndNje9q2u2OsGuJbafcDb4vuTXvxSmw/fN2iBNi7PBU1M5T&#10;IcR2z+r6hu16Qx21UVmX99owjC0CkqICRPpcjGGehe3HIvOPZL16Pk4BhGwdDzwYp/bCsefpGFS8&#10;KrR8/N7BGjvx+jF4GHp81aiIfb8RI3IsRD3aTwMDx3YNp55SQfuVDJugDfVeIqrhIzrGzjlNkjb/&#10;bv4OnCjLpkuIB2sHr7ehIfgkiSgdIkIrnna2ZVI3pXTdQFQkFVn6qqMPQ90x1jm/MWWCRO0IC/XI&#10;UdUj6rGj6mLbs4WJxb6kbHM4ZfD0iO1gwnS4ToOKgRIdNhOzCS2LYAA1h7EuS5DU57IFoXkXEnSh&#10;u1zDa2Km23fsliu2Nxu2izWrxYrtdoera+r5nPF0oq7fcUUa12ydwGTEf/gf/yP+1m/+HT787nf5&#10;5a/+Mvu2xfsKJBOQ2P3YT5mCIsZV4eX7Pc5AqaqqeCv8sFRTl+OBXDy1MW6FGo6QOzFROa/utRSI&#10;u5bFasP11QsWV9d0my0O4XIy5Xw652w6Y1Q3OWHIaRMvY5okIc73lrV9Z7G8B8sva+ckriD3H/co&#10;gsusWsyqH/xtyHnAm3+Qw5jHoHiRguYDuSgEEVJsFWR4R/IO8ToPyShuh6CmjPnh34e5Lvqvy4Rh&#10;dVUrd4cE/XewL3SdSlG0Q5xum7kLQUuvc9lrNRnxxttPGY9H3H/8iI/ee59P/+JTVjcLbY3tPV1M&#10;ELvbi83G4+jvodVl7JjW3CigibaW++NxKBNdR2gT+/icbrtjdL+lPp/hxg115QguJ76llAF/VgbZ&#10;AjhYKwzWtu17MSMFVUBZsLrKU1cqN2K9Z7/aIl1AQj7P1oNZZz/HwAJsulVKeNTA2i61i2dquyxT&#10;VUZGB5JlfkJj/pKJsI7DHwfeibzHjo1IO+84D8OOXnlz8JnD8w5XymkAcvu1u4ytUwBj+NrJsMfw&#10;3N46QLVLDvvm1+xOynWSlvr3IKIPkxzsuzwPYp1Fk+VjpCz0nDaTs5amxhlYrCwjT7QqNMFJIm5b&#10;2vWWNJtRVdpkMMZYelFpB6BTev14PO/YSlmuInrvhBxFiD24r2qQP/vDf1F8JOZmFvFYw5PT33qI&#10;JMlDrv+lIkyOQcWwW+ZwoEv1hE3wEKkaWqVfUGZZlvlNebBEc0UW2w3b7Y515mbYrrcazqga3LjB&#10;Tyb42RgZjegq0aQ/Q60xUUctQ7q6vubho0fEGOlixDd14Yc4KO3shwTQDHqXDKf3qNXnRVvVdd9Y&#10;rYAKu5CZzSc2TzKPSD83Q6Iai/e5qNlcXduyWa64eXnF6mZBt9/ik3A2mzGpR8ynUybjsSYDZcWn&#10;i6VnvLT7i6KtmS2X5EhkDO9SqyhOLJtbz3PKokBDc4U08mgYFFzkME22vu0qJbxggBV6CwlyyAG1&#10;sh05D0PBhVS5jbD3JOcJMhCYdm9FF5oLIJV7OnwuDtz2IUW6GAhGjyw+tyY+Frz9GFhmecr5RMaS&#10;mrrEi08/43vf/YiP3v+Al5++oN3scSkDuhBxhyuqQF47+qpx22/mftUclZ0XUuVpXE3tPT55fXjn&#10;COIItaerPWk6or6YMbp3TnM21T46hfbcFaDWx5FzFr8bWspy4AJWt3oCyeDCQ27liItKTseuo93s&#10;2a83dLu9znX2RKl4E0h3r8C/+jkXURk5ozCKHtnuuPn4E5bPn+O7jrFALSm3CsifKWZRlqUpZvrI&#10;IxAwlDcDj8bwvL67wCFouJVzY57Qo8+/ClwMvRd3gYmy+o9AwtBYxVR+NlwOPOTHQOPEzzDH4tR5&#10;SK7kcOZdt/cyfbddJ4MC61btRLmLHK7XGaUXkxRjVOkDHN55qlyVpp5v8F64vDjj8bM3mF2cEYil&#10;yAAiKfQcNYYTzOtn+WwDrWsw9PY4i66ZZJ50rC2BGhdVyGQpXmxKBJK7yxmCuUMgly3d/sbCd3AM&#10;Kg5Q262b5OCccqQ+b6Ofe1cyXn2mSO3als12xXaz4+r6htVqzXazJSXwzYhqNqaaTnDTEbGu6SoH&#10;XhuKkUBipKLSXDQn0AUePX6MkqAnfFMRYqCNIfNY6B0NE9EUteZ7ThwsHuccVR4Hn93XDCbSPisi&#10;hBPjKtJ/19ArNARsKakXp9tuWV0vuLm+Zr1c0a63eHFczGbcOz9nMhpTO00GKq7AFDBtUPqfiOQQ&#10;hdZKq+QZNPBJlOc4QPrDOfucOb/1nKAJoalfzpYEadcVsd4Wg6znwfckA0ED5V9KeGOm7Y2J4ELu&#10;zqvzjXekKiIN2b0vh4LvWAnaM9xhnfVJyg6JDolaTRMiGtO+NTb5c5Jbv4eQr5MJ2kRzGy7feMjZ&#10;5QXP3n6LD999nw/f+5DF1U3uW4BSEd8a1eFfYotO/3aHZ1YIKQlOM3SJkms7YiKJJgOHNtItOrr9&#10;nv1my/h8zvhiTjObEocgI8aSP9OD6SNYdSAbUB+VCOIZVL+kvHeEZlwzqyrSqGG/27Hb7di2e9rQ&#10;acNBKhSV/JweKRW2ZGJkt9mwXmipfWXN7VTMF6AZbc7RuXHc7bmwv4tiOnq95HCcAB5DgFHg/y3z&#10;+DYoOf2Yh68f7iM50B0nPyv9qeVj0JPd3vG5U+GU2z85PHnLQ9KXyDrnNBFeckm25MRz1AOReWpx&#10;JGJ0WjZKRPuBOc3xMuUv1jBLiDGwWa1ZL5eMJiNcU6Pi0KxTfVaBQoI3eHyON2jRBSdBnjkGQDJ/&#10;j/1dSeoTCa05khozp4Y3IZJKbH1oARTrw7nSOOo40/ZU7OwW6EgZUQ5cxoaMTimxrmvpdh3LxYLF&#10;zQ3rxYbtbk+XgKqimc2oz2bIZEyqKvVUWMkVolUZSZAU1J3lEtElOklUeeL2oaP2jhAjo7qhC23v&#10;RTCda5OQcjWDk8JPUfIq0EVn7mIDEwyuk8pr5rw6MQtHk6zj0LHb7QibPYtPXrJdrtjvdox8zcXF&#10;BfPxhNl4zKgeZfZD+z5zAxmFdsBLhXbbzOAikWN6aHJYN6hyGdy7rcloyotDRD+c++Pfy2sZJOsI&#10;GGaOuZPtYP2kHISL/YK274xCTi6l93yk3CMlpVzTkwlmJPUhoKCVRwGItVePxkAI2jPqxkz9M9Jb&#10;cjDYF/kezAox7gu6TEJEHttyut5w7AJNXUHtlcQtBE2Q9I4oUcuTJzUPnz1mfn7G5f0HvP/e+3z2&#10;F5+wX25hnxjcXrl6sQez5WRAbWinCFDHvA+rXPqN9NUhAkQtt3U42l1Hu7+hW22Imx3pfoc7n8O4&#10;wVU+C8GBsrB1cGvm7VAZI5LtprIFYrECXRLm0xHTixHExGq94uXNFYvVkk1rYcWh1vj5Opwoz0GF&#10;kLo965sl+/WWKiTLxiqbNbqh9TkA8hm43wIXMQM/+ph86ZicjY3iNUyJdGQoHXovjqpHjs47/nuo&#10;Q4bHUJ+86niVt+PQWJBiQJ8CMOYtPg5z9N4IOfBOmAfe3stvKvAeVFFZoUJyGkqJCD7nebiscFOU&#10;IhXVlZc9oeJzS/WEk0i727NaLJnMZkyyV9ZA2+f257MlMjBAEkNQpUDCyLn6McwGal4blSBKoiLq&#10;znZZkN91B7roBgIxD5qVk0rmZhi+dzA59ArRkOUx+DBXVRkMe+Ich0gpkUKk3e1YLVYsF0vWiyW7&#10;7Y4QoapHzGdzqvmE2FSEuiLUnuB9voTTZx40RnKDBl5djMio0qRPhKppyvOEELQ8cqBQXTKgoJPh&#10;fFW8CiWnwsaZSEqupzceMEwVF6FwmyGyP61XcHkM23bPerni+lrZNNNqSy2ei8mM+WTC2XTKdDTS&#10;xVcSEvv57PNdf2jq3AAAIABJREFUrDRQ9XXMrsMeiFituvSg4ghkpQxAzB1+y134OcdAL5frGRGV&#10;rbmekCf13gx6dVLilhmEUO5YEw1jjKVqw5gvNb1E5yDsOyRB1TjEa7LiIJxYPHblaQbjkBcKpt9s&#10;PgV1Y2ozIiVb65ImfcZoYF5dppX3hNARA9q2vPI5HKdes2TVGM4xPpvyzle+xMXDe3z03Y/46N3v&#10;svj4JakLBUkMwYv0t3zbi5AH3NsfeVySyyBMTLGoVeuiMBKHB7pdS/vymrDd4tcb6os5k/M5dTOi&#10;c+qNs7ydnlGtH7OYxw3U2yd5cfk8jV4ctQiNwOV4yrxumFQNta/oLmbcO5vw6YvnvLi5Ybne00WD&#10;hP23JFtQ9vzlf/ZPVqSvsU7//3q4BFXSFvcO2G12rG9uCPuWuqzxaAzVqkjLGlFbeZhsWWRyHsxU&#10;rkHZWwUwFAPJXr9dElxATMzx7MH7p0DCqzwYrwsq7Jxjw+bOz6VIiofnwiGYsGc4BS4QJbgie4OG&#10;rw95M2JR1lKup83TctKmOCz3KIkSc0VxmqCbq6Ni1EpFFUIqd70kUmhZXl0zm2tHad9UGehDTyUB&#10;hwKg1y2352UAQM0jnHVYL2YPAX0lhV43v1H6Fcgh1agturzAxAnOywBQ6L89PXCu1RWK61rJbwzd&#10;gjEmIrEw8dl3VM4rPsulZ140lNC1HZvVhuViyeJmwXazpe20VXhVN9STKaOLc6gbUu2z68/1m0mH&#10;CStXy9MK9BUDks2moaKzxaWeFXdoFSN5PDT2JT6DCbFqhpQ/l0M5Wh+pPzbWdnNO2VALAVfOxi39&#10;8VKkwhFDoN3s2a5WLK9uWK+UotuTuJydM24aJuMpo1FNnYV1ylrQrEIwpaY/RaGnVL7fqobK/2M/&#10;j1q7rp/2ZVNpCWEZS/LCG3znq45EVvj90FLAjL1kQ4FlWltdfb+ODD0XC20w03ahVG4ul7zGRJX0&#10;pJg5AqgrvFeymIiGKwrFtUhhzXT0zKOd4WDJXrGIjk3+Lu8cnYCP0KIt7gNGp6/3VsDMgHMjb5W8&#10;/5TjIlXQzMc8mj6mmY958OQBP/zOh3z8vR/x8sULBMeoqghtJHYdta8gPwe5NXwROvpQuh5dn62i&#10;YPoweY2YSHSIc5k8DtK+I4VAt90jNyu6+1vcvQv8bEKstI17JMedbU9kts/khCCRFLtMpgUuRhyJ&#10;xjlmTcNZVTMWx+V4zMg5rZBJLXhhfjblshYW5+f88MU1z29WrDZbQkqIr0gmjAGDT0i/JyFoCaYI&#10;MTnS5xAB2ud/VsehmB6Ad7i1l2xvFqMnRBoq2O9ZXl2zW62REDRROZNqBSjguxDZiV3PQIXJ+3QE&#10;KIbfngaGTi+/y3UY3PeRF8TWW7mSKahkAOfV42tW9ODGC3j9PI/Isdf84L2QMojOIKQYV5l5s3yl&#10;mIbtraHs4TUPdJGjYm0chp+Rfr/pl2XDx3hkoiagS8K4mrwjg4xEkFRAuJVjgwIbiYl2s+fm+RWj&#10;8Zize+daVULq14Hr9VwxPPLYi6GM1GuGEgI/wt79kjAMoS9URdAay57AgTu+WIZSkiadF8TnFt+5&#10;Jj3PhE5G7G9miIttL1rdbVlLthTs82TrMKlLuRJH7CKblVZ83FwvWK1WbHZ7oqinopmMGU0nVNMZ&#10;3aghyIDyOT9KnyNwCNVUn5goxW6yt/OOEW7q71WgdCVVTnl93v+XuzfrkWVJzsQ+c/fIPavOObcv&#10;u9XdHC5DYIYgB8IQWkBIhP6a/pEw86Q3AfMgzIsEARoIkLjMTJPsZvddTuUem7vpwczcPSKz6pzb&#10;fdkUFPfWqazMyIhwd3Ozz/aMaCGFkWQTs24AVoI1y41q4Kq9OlJgpVkbAFsmFCgCfd/iejzj/M0R&#10;t9MZqR+wcB7vVlus1wvsNEo4+AByBNYmWoBo7LmQkxKBEYvRjJk5DWShJj47jyurBlEGmNa5lghq&#10;ERPGw3o7JtWPqPw9/53cveZIs7+y1kKMlHQDsqusK7q2E2CjGsgDa5ouOoiBRRSTX4pRQlEaLzVM&#10;vKAI50UYF3oqvNLojWdXplTtKpKNJx0KGQMJg4+2F5gmQY/M8/Hr+yQMMVGCC4Ttux2WqyXePT3h&#10;3Rcf8Nd/+Vf46he/wrW/oXENwsIbsYuLTi1MrCWDocqB+eNt+amyVonJ1imT1Z5EqHp+RIKPEak/&#10;o+16DG2PxRfv4Z638EtJjWMTKiTjzfE8JJlUNI6glNAQsCBg7z2emwbPyyU23mMJwGPMQTnmvlut&#10;Ftg0DdarFTbrI3719bc4XC4Y0iCp6RS03TSLdghNIXa6N3M3yhxJ9WDW71bhM875bsf8ilp0OMsz&#10;LuI6H6SWA5cAn6TUd3vp0J0vSMMAn2LJMkAJ0SOGuhaR93J2ZUwsF7UJvFB4NrPXwAHTmUsV/7yz&#10;FvD8/c+Zz2JdQT0XRlcPLlHHScytGA9dLfWTMBcQZK91r9ZWiQwWJn+WZAWzzD2y6LKeI+XHtSGc&#10;ghMygOGcVMEm5btqUSQFGmZ5Tkm3RhpxPZxx2RyxXq0QNkutAsMSvG5zycIPHCD7n2fuLEO0tQyg&#10;yqXL9VpAmXlCuF80g11l0iUTRgZq1cJEoFBJo8kCSh7aGd7Jbg3kLAtoypoxF04WbGdBW+K2QEzg&#10;IeHWCqg4HY64XG7oBmEWfrVCs14hbDbw6wXSokHvfdYcq5n55FGRv+2/fMwBiQVpWiyFN4JxFsok&#10;erL9JUyhEmHMuR7AZEvZKVHLtbK2VCcPHiLa6w3d5Yrr8SSlyq8dAoCn9Qb77Rb79QaLpoELNTgs&#10;7Y2FIF2O+M43nWlgnFG5/HbAhHnY5nSugIqpybBYqIyBWQQJQ3zmCVpH48HvT60ZVaqbrIPP7hFm&#10;KaU7j1TP3yUqQmnGYOa1XVxixDECnEBJ6N55gdnRKaWQzI1ZajIUV5oxfk5zHgioBUvCtEYmycZJ&#10;mo1UPXqdhl0NJF+LVSD64OHWHpvFGpvNFk/vnvGz//Sf8Yu//XucDkcMQ0LQcvIZhLEW4/Feg3WR&#10;tTMCadYTQcugigDLFkcZfzabUgEaMSWMtwEjHxGHhHDr0Dzt4bcrsSiGkvobwFKoLRE8JYQUsQoe&#10;28UC20WDXQjYNQ3W3qEB4FMslhSGWIeU4S6Dw3vtxbBbLfD1yws+Ho+4dD36NEAAtpSNN+CeTJOu&#10;eONjEpyjvO8fWLx22CPNVCD5Kwt4VXbgMLYdLseTVt2NBQBoTQTFUpMLSxo8Zd5T3WCi8WeFxKwV&#10;M2CRrRzV802eda76Ts7h2c+jY+q6uQcKtSo0ve8jy8Wd6/7BeY+u9eg62eXspp9bQPhr4KK+r9nV&#10;hceyAhaTy0KDuaaNgyqz9hmpIUDWsb22+PjNt/CLgGf3HmG5AEQtAMDZYSG1R4rFnW3fy5NN52ey&#10;Oq8AcCaEu4XJ5i8UU47DRIBkDZVMlKYJHThyE3k1RTeKjqAkUhGDlNog0MBI/YDu1uJyvOJ0OOB8&#10;umCIEc4HhNUqtzx36xXiUjqU9iTaZkMhu2J+06P40in/LbXcSxbIJDATU1AxIUyiifAsHkk1RbGW&#10;jk2cG8qkGDF0La7nMw4fX3A9nsHjiJVv8H63w9Nmi81qg0UICJqK+9Zmngope38mySrBZci2DGFW&#10;6nYWtFvPmQAJGbcFvBpduDd+fypVsB6TsZHsJzf0Di1kVVcVtO8+uKZtqMl7rKmPEk0KRAYHJ6Wn&#10;vfpMrcQ4qhTP2T3y2xNSkHn2ZGCW4JiknF3SGAcDcjClTPebdqjMWk01PiKAPWGxW+Mnf/C7+PDD&#10;L/Hjn/4E//Gv/wb/8He/QHe+QWwPBN/4DLbGOEggrfOgVFJlM8wwAGHvmBpMZk6VH3bAyAnkHRY+&#10;gEdGfDmjvbQYzzeE5y1W759A6wVoGbIJ1ydGYEYgxn61xHa9xG61xGbRYEEOC7CWBR+rYHPO6eCs&#10;dEsOcGnEfhmw/Z33eLdb4ZvDCl9/PODlfMW57aRgoAtgJ+aZZECLpAvzqwQ41zp+S4fpAHnUJhCq&#10;RyJW15yuT9u2uBwO6K83eE5Z0VOkCgCq9SK7RUwAWo0Lu/6jbI5k/Skynnh7f83Pee2z6ahfmevZ&#10;DebXVdtxOd2sAzNQUH//cwDGa+UT5s9CjrKiPLWYaBzbK8Ai/82qoJv10qwezCJrnZP971jcfhr3&#10;IIxXEgesjvQ4jji+HLNS//7LL+CXAZxGTMGy8JBoGMhR2eOfOO7BosiMcHei/mvNqCQokSYd4ghF&#10;Y5HpdxkVC6HL0hZtzqwZeu0oWpoQIhVLBgAeE7rjBZfDCYfDAddri2EYwM4jbDdYbLYIG6lRkRqP&#10;3hFGbz5blxH5p6fkjcniMhekwrH2mYUQcuZH8VWViXa1Gc2mm8p2IWv+ljgrTCZgiIEA6QswdB3O&#10;xyNOHz/ier4gjSMCCE/bJ2xXa+y2GywXS3hlGKw+4wl8qAg4qXvkjhgmzLQCFra5MuLW8YWQ5+YR&#10;cCHT3Hl+1U8fjyxONP9Q0S+Z2K32CEPdVCyxLYm0dbJmQjFz3uB3flk1LxjglVxwWc+STSK/EaQ+&#10;RgoOgJcKoMIX4BJlsDQdU30v5PuXhFXNZ/fCNHIztMSmCOljltFSfm6Zf4sJIUfwq4D96hmr9Qqb&#10;5z2e37/HL//+F/j4zbforh2G2GPhpV+Bi3I9753OExVrpO11hqSAa10YKcNte0DozCwfrMDMk7iq&#10;xjggpRO6rgWliOZpg7BbwjceHoQlEfaLBfarFXbbBVaLgIXGVbgksUQewmOKQRd53MwKfJjhWeMn&#10;nEdYB6zCM57WC3z1csBXH494ObcYUkKiBnDiKrFqi8XC+0iw/faBxfzO2YrFor2Ci3vS6b7jYUB3&#10;uaK9XMGjVDN1UFChgZTpAR8QK6MomPbzeMQV8J2BC3vWT+33twFGXoTq75ofzIQePbZSzq87Bwz2&#10;+5GrpP7ua4DktXM5cd4zJjfAprcVZSADChurytdpAw7k962suERnOW0bXwI6WX3nEU5rEhEoOHAa&#10;cPx4gG88lkvpcO2tyihDXMuZG0WA3f0cPzjMmPLgExA5hHri848LYqnwM0RVvjsVYgqtcyBITZRU&#10;wIURb1LN3JH0/RjHiK5rcb212n72gO4quevkPRb7vdSoWC1ByyXQeIxeguJ6DeITvkOQAmC/PhOY&#10;ZD+Qgiwq2TB52istvUx2VfeimpoaWMibuiosgZBOwVlKCTxGXM4nDLcO19MZl/MZQ9cieI+9gYrN&#10;Fo1aT5AATmLkssJLjqbarE1Hvblmo86vjHnl7a2EPa9ZAiC3463Hb4Ou05XrmAqjyNdiLmAmvodP&#10;R3fvc5o9KyrtibS7LJHmksucDCm9yjTrdS35+vpxAjgljDHBRQcOXp4duukVAVgmUT2n5Q7Iloh6&#10;LfLzQqwiHlLnYiSaWF8MPOV1UiBbM8iYIkJowOQwjCOwIHzxox9gs13jRz/9MX7+s7/FV7/8Ci9f&#10;f4ux6xE5CmDQOhZEvpidYXULUgEYLGCHibXJm7l/tGKu11otMQLO5xLU/RiRrgOuv+rhryts3u2w&#10;fLfH826Ld5uNZDctGwRPCF4L0cUIThFELFYORxILzYzS1rpaP6iOE0dgHOAB7IPD+mmDVeOxW6/x&#10;Dx9PeLl0OLcD+nEAfIAjnysZklmT/gnBxFuHbKPK/40CLBCTAIvzGanv4FhrAbFy4SSxXURQ9yjN&#10;iJXzPpzcc8I3CrB4TeB+Kq7hLTDwyfE/+O5EPj249OdZSz7/fnM3y1tHYt1DyofqZyYyaM6Tvyeu&#10;S1PqVcmLLCBagD5DIsYlTZ3JwREjqowJFOC8Q+xHHL85oPENkBJ2Tzu4hXQijixZkWSuXpXnnz7u&#10;zylzQmK5qE0+zjnNT5+abOpJNa228FwFKKrtJCrYy8x1DDULkVoqGEBKiF2P7nzF+eWEy+GIa9uh&#10;Twy3aLB4esJiu4ZfLcChyS3PkyONntWALJD0EeECDj7neKScEMRd5sniCdR6Q1VMgfUKme3L/Pfs&#10;ujkP3GRv0oZrUObMCeMwort1GK4tXr7+CsOtQxoGNKHBD/bP2G422K5WaHwj3zOfHpL0tNBnixyz&#10;NKsRMmCpVMA893zyrNVAHO5Bhas2lFmbRJGtAYqhGS7GBhKNg50xuPL+/Pd8/QpQ5ckb+Vnn59np&#10;r9BvzNYJ08Dqy1h/l+KSsGqqnAThe4akVmqGiuMEBA8KYma/BxZFVDEhF/ea62fmEpIiZlpt1SXE&#10;lLT4lugXQmNc6n7YNWw9vEPihMTaFyZICe1t84TN8x5P75/w0+MJL199ja9++TW++eVXOB+O4DGB&#10;nAgc2a+sGpNuYhiQSdmaVdA4YLFGOaDZi0adEJGS1OpoQKC+w6ID9rzFl8slfufpGU97BcxguDjC&#10;R86Bt5ZazEnScWtLjfAp1eA19iKlMYMMOVH6Ju3XSyxXK2z2T/jVywk///qAj6crhqStro2JFxsj&#10;vtuhjPk3zCJ57a4PLWDK85zSVRpGXE8n3I4n8DBKXozSacla0y8yMj0RicAy8PgWuDDrRp0Zcnfe&#10;jPc8us7nH/ccYXJt1kfOvON1UHN35VesFhMFVedGLzi5w4T/5++i7AmSGZ67Q6qhyHhQwIYU0avc&#10;KWa10AxMItImmeqa0n0o+yUhugBPQWIyxlJ6oL/c8M0vv0IaR6ThB9i922OxWpb1d2IgeIAty6PO&#10;gehsaaj6J8zLc8/jKeYLYZNt17BYgSw89OGcMaLKEsCwyouEvpfaDJfDEbeXE8ZrBx4T2DsJ/Fov&#10;sdis4ZcNODgxbztztWjAifq1BFyUGfmUz34yEQ/eC24asGlosvZrE5mlxObZNpxduJZYBWgBKowV&#10;eSKOGIYB19MZh2+PuJ1O4HFAII/9eovn/R5Pmw0aHzLjtvLOsSqfS0pgQsri7TKfXdZ6P3NfkwJA&#10;KwJWWytsE9QT+KiPSFIrVg0a7Ec2jNLP7Dc9Qmf5MNBijgS9j1pEKD/f/TfrufDwWgDGgl0NYKg5&#10;X4WEjcmsVfY6P0pUi0JiqWWTGLAy4pT5hhTtqp7JXmfrmM270o8VKpJ4Hi3Kk7+cSnqyzaNZquw6&#10;xGBIXiw7yuO04Nvt+z12zzt8+MEHfPidL/HtD7/Ey9ff4PDtAefzGbdbV9qwK9OphTiT+R0NXKhP&#10;yBhqihrsLVVwGUBEBEiUgffPT/jwxTv8zo9+iA9ffsBqswIcgzGACOICkYmAWSPUtCJamxUbqtyK&#10;YFNYdJWoaOASuJngvcMiNHi3XCOSRxuBEcD52mOIutaZaH8NgDHhvr8ZwHjrKL55oTmJ5SMgSsfj&#10;7nJFf7sCMYlLmxO0aIpe4MHjqSXVQEMexcyNUM598H79OfCqDPlNAcZdjEUNMoApT8b960fPNTnm&#10;lttKoc6yr3q/XnHClGr4AXio16/+zI7E5uoyy5z9QPmCgn9IjRwiJ2usJffHcYR3hMaalg0SJwgQ&#10;xluPl28+5oaWi9CAvMlohdV855gpU1O9pkd0BIbB89Boh9LaemEV9WqBkmeq0oIBTZdEEg0838zy&#10;2Fi7wklwSewjhrZDe21xOZ1xOB5xvbaIYDSLBZr9Fn61hN/vkYJD9F78SZTryuWNZPix+IYYUrZc&#10;tbva/DQBoZSBgB2uEqKOCMHVVy5E4Cp0X0zBqu2Q7U+BVtJ9roAPqGnMNIQ0jhiuHW7nM66HE66X&#10;C1InTYW+eH6P1XKJ9XIpDcX04gRjfio88spwBnU51fO1DQghxgI2yudEDt4TfFjk9ul5jDX5ZKFb&#10;7o4KaI0pgZ1WCtCo/ETIHRUdmdBD5SHSWIHJxlf4OkOLZBWwJhYQLhVwUY+V7uZBQCPALI3B6sBP&#10;AxJlZrXaJ0SKifww87vQEUUWa1RPSJ6A1SK3DWcCotGas+exfVWsIwaw7K6U6UfAcwgeTotRDeOo&#10;mpqKAaUHEjJTDUZdkVw62QIEKTfM8IGw3K3x5WqBL37nC3TXG44vJ5wORxxeDrhdW5zPZ1wuF/Td&#10;gDGNNgBhZmqKlWqaSZmhWbeEIkaWGh4IDsv1Ek9PT3jebfHPfvIT7Hc7rNdLNKsGcIzBJQwulvTx&#10;PBlc/eu0Yqem7ml1SYlVKrFPDJe7pJpVTVxjsrrOOfgmYLVe4int4Zoebdej60YM/SDzT0bTtYC6&#10;Z7iP9tf3fcxva/SSx5YYHkAaIq6HI9rTBRiitGVJDGd0rbTGaRqUZmZsVPQIA6wTUAEgv3cvuO17&#10;8/fvxvNZ4KIe9Px+8nkBEcCj58lXmlkL3nJz2J0JRVbUFtBPPTurNm/KSv0Mk+EofdUgJluYqcQw&#10;1eBC3BYOMXLRxHI+PKmJ1SHBYbQqogCYo4B2J8p+bAccvvkI5oS+7/H8xXuE1UJluchrG4tRhmVC&#10;ZjnzytzVR5hXHbs7bKZsTvRiMcZMTI1WpIxjnDQmYuGsSGlE3/a4Hs44H064HE+IMWEE4BYLhO0a&#10;zVayP7hpEB2VgBV7DFsPIBdyytpTHuyUgGpiycOpF7NaQIupkC6Q9wREZAWTXAYvPgtpnjEZhR6c&#10;tNqaABiOEXGIGLWp2unjAd31ChoTVs0CT+8+YLVeo1k0aIJD4wO8bfJMUMjaRUkZnS545DhJqyzP&#10;Veam7JHK9aHpiM57TMhnNh1VCNKD68tzFmXCgpvEXA9HSKMEllrBq6ggwSvytgpwXgvHzI8slPMG&#10;0w88CnN86zDQNLu2Mdm6fbtZWeqrakN0EEsHCx+LthcjMBAhBZ8bonkrLJcYERLTYM+RTcu1daKG&#10;NhWQEyuCxClFTgUUIYtb3Secvy6xEZSV8UQSkGnl2+AdQligWS2wetrhi/4HGPsBQ9fjer3icr3i&#10;ernh1t7QtQOGYUDbdgLKRmSGJD5bDyJgETyaZYNms0JYr7DYrLDdbfH89IT9Zo39ciWxMA4gjLrP&#10;BVAJQ/NVUJsDKFX07YpwsTXSqHaL3QE5RLAGVhv9CY0n5zDGhFGLSjWLBmuI2yj4AZ13GIa+inMp&#10;dFKxwgl9/DaPrEGzzIBjwDPBpYTu1uL88QXD7Qqnzf2yoOQCkOUSPLGaFTcHT4DF1B2Cuwm4E7av&#10;CO5Xx/Pgs8f78tE1pgD0rXu8tk6vBWzWwOJRfMWjmJLXXEAP783GGx/xt/u/GcoXc90MAyS6d8ga&#10;pEnoQeQIaEEu5wg0RuHtLH+PPODl64/o+wG3W4v3X36BzXaD4F3OnJIeYSLvLBi+GBzu6WAiAwAE&#10;ci4zRlECVBsziTCbaD2tXCgxkvosxYQropkTAUNCf7nhdjzjejjjer6g6wdEQGpU7NZw2w14LdU0&#10;x+AklqKyIc8wKGxmi2JTE4TVl8irBxP1mUmwdQOkzHBMsyFSbRS4s3bUmRG+Qnb584r4kgk4Vs0B&#10;BI7iCz29HHE5HhDbDhgT1s0C++cn7DZbKdHtLQ0wZWZrxMTMGJHgaWpmn88T54HT9L3q3GzqdlK0&#10;y0qVi8ArY74zhwLZHYWKHmCAhRjBORGinLQSJCo3gaVKyVzHDLwp+45N+NbVQVmtYADnnh8298Zw&#10;60iSOuPnTuGs3nDkJfNDM0o4pTzXGVjoUHOQo02vPTtjkmo79oMULvMOngNc0wCkmRTM2pDLFq3S&#10;wFRAAlwEpY1Bx+oIcCHAp4QRESOQ61UAFq2exbLsRbt3RSgCThlm/XCO4JcBYeGx4hWQEp7Ts5QX&#10;HiOGYUDf9xiGEcMg/U7GMarlh/Neco7ggxNL5HqJsFzAB6ly6p1H4xSkwxpomQ1O0h/FxTrXjlwG&#10;0PXalhNqOidYdWBzH4lQFUYJ8gIurOWB9/ANsFCLVhM8bi1hGGSskmGl7uLyBDNNdJqhlQnkez7q&#10;e5hr1bF0zOVhRHe64HY8IfWDlE2358gLz/a/ETQqglBrmSofSoP1yDiDixK4T5g+Vzn3bYH/5jm1&#10;gqB75NFskAKhu8+N+dZv1feaCfT8WcVT6md8zWLxCHy8CTaVP060r+p+9v3EMrbX3CKqTxZwYfIJ&#10;5dQIBYlIQHIa4CndhZkloithxPHjC9r2hr7r8OWPpFdRsOq/+tCT/3ItjbdJnMAIectmYXEfpDGf&#10;8MzYGeAoAsSRg3diwejaHkPXozvfcH454nQ4om970ZJWKyy2kvnhtmvwskEK2r9BZliKUlXM1RhN&#10;qWUwe7bJXra0vSkaLWtEuZBQdgUZg6+FxoNx18e9taIcTq0aaYwY+l7av18uuLwccT0d4aI0QNvt&#10;d3je7rFdrxCcxFQkTvBwKohSFiqgwoCTe9y1thZCNm+PDl8F7Mr4XUXE0+vVGyvrkvMKmrUgZzHP&#10;2pueSgqzA+WCRQICDW1zjqiWwFzKYMUqxxKUiZJ+Xldn5mlw46eOyZgysNS6FZCARSSWoEgDpoDE&#10;GlQTbfdyBi5YrFSepXy4NUTjCFDwCKYJqKWGCRPnFmAxH5VMwOSWxQLqHIKuRUpJzKA5cM8gtYEf&#10;ASW2j8S6qClyWpEvWnVKaJdzjRwnBzRNg2azwJa3WYOymIdkXIiA3GeAICgxxysJkWTyymXaOQMJ&#10;QOJW2MZ6t5CF58wPK9BWGLBUGnUwJg2Qk7gtJjEZj5qqx17iwFymSQC0hPcORB2GoYy3fpZH/n7A&#10;BMT9M36fh9pcRTNNBJ8Y/a1Dd7ogtT1IG5QJLyvrY3NrV6keuowhj5MrMFGOYvaffRd4ld985xiL&#10;2emVCnN3As1eFSvHG4L+NRDwCgB57flfs2o8Omfy6LXyUoGImpbq9x+CC1TgopJfmRGZAHXGYzUu&#10;I2nzP3B2n/a3Ht/86mvEYcQPvvwBnt+/w2K90n5GKk/JAkvvxzmdy/J5uEdbdgHzOyLv9HxWYi3k&#10;I2lxAR4cI/pbi7EdxP1xPuN0uaKNI6In4HmLxXYNWq+BZYPkPZLzVYkMgidJ7SPNxkj6U4IBp8aY&#10;HMRezed8gDKRVrND/NY++47lDAMUdszLQ+er6lzFGLMrQTRq9dc7KVWOBMShx+1yweV4wvlwRH9r&#10;EQDslysI1W+GAAAgAElEQVQ8rbZYLRfS+jwELe87guE1FiBONlipy2AgY/5glBljPV+WlQAjXqgw&#10;9QYuXCYwI3iz8EwsN7a9qcxPDmCtGJD53Ut7c4m1cKy9M5IIMcdQ60yJQXGOtD+J082FrIGiBpo5&#10;6l12k1krZHxKy68nw+RzbT1ZtZ8c8EkEYql5IHVmpAdMbZq9i0FR7dto0bONgRGTtnWPHq5ZwAdX&#10;QAUR4CmnlaZPMGHSRbCGSd45eJb+Ko4iEqSduwCLKq7GKjFqrIi42grTzMzPgIAr7yVoSijyUoA0&#10;Oyk7z7L80r44GmtiWQWmATNrjRFH1V62hbXuOQaMUv5szs9o9iKLSdvP+gULzTG6FHcJ0I0j2hgx&#10;MIvlh4QeiR18ECuGcx7e+2ytiXGU6qmpooO8D8rDmCLwuqj9vKNWruqr2VusYNIxwENCd77idrqA&#10;xqRuXd2fKK43oTvKn1n4vTwuozSYsTl9PAbbCzT5G2X8s/feOh4raI/47/x6dh9b/fv7PYq9+JzP&#10;Hn3+6NxHFov5ePJnulHqq04+I/vhu6EUuJR0X+taMlDiney7rOZfPRcAO2VjpHIWwkdjBHxwcMGh&#10;v/b4OHyDse3R3Vo8f/FeXPTLBZiAIUXpR6J7//H81OvAUqGzRk2z4eskVJqTnuecE18fEcZ+xOUk&#10;Ra/a4w39uUU3jhgcgbYrLPYb0HYFv1oCiwajITedIZfUZwsuqavGGHSvsi6O8g/kXw/GWKM+syJ4&#10;L8xChM+MsGav68yC+rcduTWu3ieEIIwziqWiO7c4fTzidHzB2PXwDKxDg6ftBrvNFsumQeO9Nmcr&#10;fnMxWXGFSi0pTv2hJAg0E7DJhAogyfAsAtnl562zPsqPsnMjFipi862tHmPM97fYlTw3zHCREIeI&#10;vu3Rjj1SP6KPI9IwYkhR15cyyPAkhbkWPoBCwHK9EvryAS6UwEgAmqYlOyUDfbb50SDSz00XsrXl&#10;6boSGIm1GJrVVFB6nWumsnmFbnMWSEoI+gwOXKwKEVIboxGQy86DEpA8KQjT9c6ZDg8f+E5bFqAh&#10;LZc9J5jhSAS6uh+SKgsVDeVGTFQEI+X15cy0coG7fEuhL7O6GH803hE5SgYNSuE50oaIOcOJTDAY&#10;T9WSxbY9dXPX7q1Hh+0Ra8Bor52CGyKzRshniYE+RgwxYQQVxqvjML90CNqELwT0fa8/g7qA33qi&#10;3/yYr/0DsQmz8DomjF2P6+mM4XrLblhU/UEyIzVrVnXdDCzYzkWxHH0CH7zmKvic9x6dMxX+s/Gi&#10;FuDy7neNd3krBuIty8OnnnX+XpZVk336GKoxc8lQoiK4J3OBQrsga66n/JoJBVwYsKWcsVb80Xqt&#10;JC4RZwrEKO87khTu88cj+q7D6XTCuy/e4/mDgAxvCRyVkvX2wRLQaZNojPYu0EQ3LlloP4RZdcOI&#10;/tbifJQo8+vlimFMSOQR9mss1xs02zWwapCCBwcJ1IpJQrgcV4xNdz87GW9+fEIOnFP5mp8HJkQh&#10;DNAYhIPTgDGfM0CsANajY/q+skm210Vjz+c7yv5m76WvxTgOuFwu6M43dC8XdMcbxqHHZrnCu/0O&#10;u80G66ZB8CJYCEBKY6kR4oUwOCHX0TBUV2uWzGVOJgCAy3ukXSalMdY0E8YmlXVuZZQ8GeRrRcjy&#10;GWyARd5JMWEYBsRhkF4wL2fEPmHoevTjgDiMiByRxoTICZ4oWzRYLUnkJYDVBS+xCk3AcrlEs2yw&#10;WCwQFg2WywYueKmZAC+lnmG80QICcWdZmB932kU1QqepmwTkIF8k0rgGzn1ZWCddwACy8CIALqo/&#10;WN1NxLJfUoqltLJ3Wh/XqbUHuXMv10DlNSY2A76SmeO0b4tGNOiFLPYAYFV3oHupAM5oVUzBVT2U&#10;B2xEGRtnk4i8V8yzsp5m0aotMhmE2J7nyrdVoxQwqDI/GXCePUaJSSFks5FQ9iiBa7BraQotGDFJ&#10;9dOkwEKISMsqc8rpwDKdcoWmaewdODcipTLvuUneb/EgKCB3hNgPuJ7PuJ0vSEOETzYHuv7Vbiia&#10;dtnuWVG2PT/RtF87OPNevMIrvt+jCO0JsM9CeMa/uYjyt4DEWy7vRxaYT13ru1hr6mslVQQmrpHq&#10;MCDhSCySWcBnxU42NBtDNHWThLwpN3YigCSRwmtgJxhiWXUEggdHRnu5oes7tF2Ltm3x9P4dNvsd&#10;wrLJvHs6N8ZTqn3OKEW07CRLaaPZxNoViQmpH3C5XHE6nHA6nXG7XTHGCPIBfr9Bs9nDLRuExRIu&#10;eMl1d8qUYpL6/RDmZqZSSYGhcl4lQLMbCYXRGLusu5lazn8wLc77SfBl0oY89SLWxEksQV5iWc6w&#10;RQMLOW/TNCYEcvDeYRwGnC9HHA4HnE9nxFuLZnTYLTfYv3+P3WaDhfdonNOIf+Rdku+slQfLPOha&#10;mOAgAwM1Ekaeo/xaJlXaYDtfwFWl+QN0v3H0Qg7uDkgVZl+0HLbvJCAOI27tDefjCefjEf21B1pZ&#10;5xS1qCwLkuAEJI6ImhXiLC+bI5gTbqq5siNpX+89nHcIwaFZLrHdrtAsl1htN/BNRLPw6lNUl4qm&#10;RgpP5TwuS10ub2UUhvlBJAVqCMiCl9nB63gioprGLYOhAAxGzgaDSyWw16tGETV7inkUX38CaCHf&#10;SV4YQkKSDoeY0mgd42NCe86MWNcmBIvZAZKLSFGBDdfuBr2HIghLUyTns+VrfsyFCc1ibxjiInTw&#10;5bkqa1hNg8IbK1SWT1CUXFktYLRXrRHbu7rnTfEDSAFg0r1WbsEsmUk5oNO5irILJyYSnmRpygyG&#10;Dx4LEkvGMAyIMWkgq2WG2f5TRv9g/r6vQ2pbkLbV7nB+OaK7XEApCrDI2qU9xYOAfC7US7WQQKGj&#10;VwV0Pv27j/KTQY+vHraYn/58Erz+xv0+9RnwNgCpP/8uwCL3DCnvwtbotWfKK8oWWGluEflcJZNa&#10;WgV5syu8ybPFeUkWFTsp4Bi0h0hkqJvNiasWI06HI263FsfjCU/v3+H53TPW+y38ohHlWG+cJXCl&#10;HACEIP0onI1cg9MIMSbA+1ywhpgwtgPa4xnXlxOu5wuu1w5dioiNA1YbLJ/2WOy2QAga/OERSRCV&#10;DRKgyifOZUPa4G3RqrWq8//zRCq3yFkeTrRf6VjqkAt1m+k1C0mNTyCH0rmuQBQmV6qpqbDlCJCm&#10;dzrIAsSuw+1yw+HjAcfDC7q2hXce+8UKX7x/h9WiwWqxRBO0iZpVZTS/GKjMBwCOBqgoqxPFrFaY&#10;gkyXtpNXQiOSrI+kmvKyCYCNnwTHshGDBvmUeVRpyFE0vBSRkLJ1RQSZMiQt106eMHYjrqcrLqcz&#10;zi8nXI9npGEAGPDstUIkgTlCWnQngDyYSnU5Z/hNGbTwuwSXAlKMAEVxnzgCXTucjxeQI6w2ayxX&#10;C+z2G2z2O6xWkpEASIwDGUhWU7ykacka5PkiOcua6BmRMGNi4ZFfXqpNOg/PAWkYVHstRZ6E1qKC&#10;15CJNO81gdLyXiIQJzAPSGOUdFXv4HxACoTBkeybjCJruldQUAll2zc2DqeWF6ExQgqU1z7VnVfN&#10;x84MV+V5G5SujznDo1qIThirfTtL2wJ8DZ5PhFMRgMUmxJCaNbYPSZmY7AudTrAFbEGm3szCYrBJ&#10;mhrrEZMDKCCRR9e1GEZ1qxniYAWnihBTijnANUHS8sxfLdbARgDKMIKHlCvuWrGzmAX347n71MEV&#10;TVUrL6+SAFcfAR8TbpcbbocjUt/Bg4VPwTAOVdcjoHIn5tUzIKGYjvIEV3e1OS4i7m5c86DFWf5M&#10;fh+owP3d58YQ7PeDmSkbanLfR1P8OcDgcywTbwGIR2BAaFziiozP5OEBWiRv9o2sUOg1mQufJso1&#10;fRgJdWq8km92UWZlJ2mgOWvQtGN4p7GBGo8h7hgHVotntuBB3qdEGFLEaTiju/S4Hq94eveEp3fP&#10;2O638N4jkQWDG1/VWlOcEABGjOJ4kWAPbaCkAVD9OOJ6aTHeBnTnK64vJwyXFn03IjEhbFZo9htg&#10;twLtVuDlAlKyXpnfxPQsm6+Y+SeUcL8ZzfRoWpUxTd0dTov2eC3R7bxHjqTWoDrAgIXFGQCWfpvr&#10;GViwAplA1nh9FqEvi+SAMSEOI86nC67HE86nE8a2RwPCfv2E9WqD9XKBzWZpwfJQKDlDeDXrweQ5&#10;CSIAiKwMNWYAaTZ/3lkhDYCCAo2g51OZY2ZlVpTdsUQQZsmi8QujoXxLM3NLZiwBQ8QwJAxth8vp&#10;itPLCdfzGUPbg4cozNUFXNMobpkQQF5+XCCQb4AAqW/vVAibqm8pUpyQRgalmKP0kzYdG2NCjCNu&#10;5yt8e8PxdsP6dMZ2u8Z2t8VmIzVCIo9TxqdM1Jiqy0maxfJVb1SFIsVdyRogaaZ3ZlAi6dCZAOYI&#10;l8FSFUTFulZsVqoqOJI5561TYgEuASCWWgypAtqw52LCo7ofdmRIlLhKQ5V1d6S9T5QmiB1SFTxm&#10;accGXeZs9CFjvbtzhspTk2n+995SVL4ogOJ+RFR4A+taZouTzmq+lyAL54XBRQ2+ZHKg0GCMjGs/&#10;YGTZ48xVQKa6fJNaJCyGpzy88QjJ/29IrD7EUk4emg5sbjNzFd3N2Wdp7lxAd14iTeFlwCVGYAJ3&#10;A7rjCbG9waUkMUIZ1OXZK7KaDcJN+Yk9VwE1rMOdgQfLu/7EUQOU+8/KEF8bO799AkyMvhqj8cb0&#10;1nR8H+fxitXgE2v26uf3LL48aw1abNdRFhW6XrULlPIgudqf5kotcCMzfHmt/UckSSKpQq2yTe8p&#10;pQJEAXRMQJTKwJ4FEXACurFDGiKGa4fu3GL44h12T3sst2t47zGmqLxUsANTQgihkWC0FEEMadsd&#10;GWlM6K4dzuczDi8ntNcbxm7A2A4iBIJHWK3Q7LcIT1vweoHBO9VKdXyvrMenNlcNQJiLhmsBZ+Z/&#10;svLUPpcnLhqd+LCq6ynay1YQvRbra2Nctj89KYuNwvxjP+J6lP4nt/MZqR8BZmwWS+w2G2zXGywX&#10;KwTvwDwqHzKCYHwKIU+mS534dSBddkdAicJpPIWXEssk5hsBJwoiXHa4yTVLcFz1HkG01sy4xSct&#10;cwCxVqSE1A3oTlf0lxbnlwPaS4db24p5uQnAYoHBAaMnNLt3QPDwzRJh4eHDAq4RcMEeAn4cA+SF&#10;8KVNF8AOCRFEHsxRO5BqWugYQWMC0oDUjeAYMYw9hnHE5XTGZhix6wes10usVwHOU06BzXMIaIE3&#10;pxkeJAK2+txSrSbtpO0aukA+eLFeEUmdfk0BTSQ9aEwWQcFiLWUckQgjLlY0RBVMnJCS1GJwXtIp&#10;odpVYhbAogJnTjN1VdYcj2PgQX87ZfreOR27Al+W8Rq4MCOAkl2hPftN1Xw+pOVPC6C3jrwzK5dK&#10;3qt6fZq9gwpHi0DNcA4ghzEy2nFEF0cMYIwEDCwxQEjyY7VOAM5y1Cyetp75CQloQoAnj+gj4hgx&#10;jmMu3f6bHvMZFKpVyykkLft8ueJwOCAOIwKEfh65kO+eZgb88pxheu7nBmL+Ux2vBmHqP4/Aw6Nr&#10;vHbOo/fvskBeuc5r39N3UYPi2oIyv/7keWCZPyyNGEl6kAi9KX+nZFqjvK7LsyT5g03LIkIEVKGS&#10;QHyXAOeCKPIZ4wq9D63Ez/Vth/Z6xf79M54/vMN6t9F4DK/pq2IhCUM/wgEI3gMpoR96dG2P2/mK&#10;8+mM6/mKru0Qo2iWEYwUHFb7DRb7PfxmBSwaadxkTOoTe+tTwS9TJknZRE9qZjJfuIdaLGb3q3t+&#10;6KpMdk1iq1wmwTEWrCfXlDXxYHCM6K43tJcb2usF18MJt+sV66bBdrXCbr3BarXCwgd4R/oDpDS9&#10;/9uM+NH8WJzI9BpmdnJOKmlasCa8anLWEA5SLL6UFCtGbienZaFhXLNuu+udl7YnAyMOHYZLh9vp&#10;jOvHI26XFrHrERMQHUDrJWi7RrPfIOw2wGoBXi4A7+FDAAUPsoqfJNp/VEFJGnMxX3urdwIoBklA&#10;SAyf1Fw4RFBM4LHXINIefUw4gXHtO+wDsIRH03h45yVFNxW878xyNVGwVHswK1nWGORZJjEtatGh&#10;JiA5QkpSfTVx0lTQKsuhWvM6tsleZisJR6SoAvOW4IIEtVLwSI4QIbVg2JgDKuxQ/RRApPdEdU4G&#10;PPaj/yoDylk3XMyzxqQtmwiwALOaXqeLWNIDf72Dsur22vxVA7XParARzUTrweQxEqGPI7phwMiM&#10;kRMGZgxJ2toTW12IavKMB6HSJvW22RIG0noYGvtCQEyaDmxgbSZ0PosHqCLF5U/hg4AWzXKI44DL&#10;4YDb+QJngEjPK+Mo95yywKK4let/xmH08anTPvd6/2hHtau/A0B6FDw9f39u+ag/n1jQ8GkwYp8/&#10;BBJ0/+zizCjXSCy8PKllQ3iWAAtxk0OIVwsDWq0C6dDKuQaGCFpN+yfhRVIeQSwYJO3Roa2FMHDC&#10;KQ643W44HY8Sj/H+HTbbDVzjs7s/eBck77WP6G4tLpczji9HtJcb+ssNaWQty0zS5yME+M0a/nkH&#10;2m0xNh7RiFeDFiPHT63nm4cFGOVeHA6ljKlaLDLSppkJl7U2ulK4is5K6y/fsfoUYBEG3jlJIUwJ&#10;463F9XzC8eMLzocT4jBguVjgw36Pp+0Oy0ZiKryVT9dg0aTtz+0BjUHXlpRHPQrqI3vMawbuvLoo&#10;PHzwGZ1KMBuVsUHM52DVyskB2X9vPkuoxpok2NGVYB9ihwUChnbA7XzD6XDA+eWMy/WCvuvRMwPB&#10;YfG8x/r9E5ZPT3DrJWgZQKFB8g4DEeDlGVnpxgRXAqSvCD1gtMrEvbkRzLRMAPkSNc2NBR2v5Ess&#10;tSQ61T5jHLBxHksfpIQ6WMyDJkjIwaWUY2gy+FIEIYHVlQZauQsy32aJ5fDBwXEAe9FcE6LEU+Qh&#10;TRkCoAGcGaxktULcWFE0CMQEigwsWDNoJC5grBknVV+3S7HKRpq9b691PKTPARsX1amlVBgQaiCq&#10;n3OVMTNZP57sr8fHp8QOTbKVMtCrrv9IXtT8QOZBrHfkJaWZImMYE/oxYkhS2TSyWOksuNHsJPlf&#10;9YtlC6TSp9W6sH+lIJlHcASXPGgUl16M8Q5gfF5WgY1X6cxgIENcXonRni84H49AHKXoYKp7H1VW&#10;W1JBx0C9KsxF4SjcA5Wb6IHgfGXp5uN5NY36je9Mvv8bWn5YbvCdXRyPnum1uIxHsRi18iCs6rF1&#10;ApnSXp8Hrj43wGip2g/P098WZAnWtHxtxyHZY7ZvjbZF9rEqW3Ci/EkfGtIieQ6ckmRcmjUVUgeo&#10;H1uM/Yih7dBdbnh+X1wl1AQEjIzb7Ybjxxdcjmd0bYfu1kp6ypgQ4OHh0KeE5B3cYoHl+2dgu0K/&#10;cCI4wPBM8FyI9lPWizw5s41XzHiUByRVqa2a5Dx1dHpQyUmT6xclpLJqkgZYcg4AJWaM3YCuG9Ad&#10;rrgcj7ieBFR4AJv1RjqU7nZY+GDhkmAVCjKE4i+8IxoTkqCHzNGORMq8lMGbVuS9aOHWHMpQJlFJ&#10;DRJ5QMWNxJaUZjdPsG6l9jlcYUAUGRhHnL49oz9LlPDhdETb9UjOIex32OzWWL1/BtYr+O0SvFiC&#10;swavIEIrIZKa8LM1os7OUEGIykLAAFyiXC3R6trb93M1y+BlU5DT8JgGiRI4AZQS0PcgIkRfaj84&#10;EAIJjToFGAF2L4mgzsG0zuJkMofOBZ5qus4ZG042ndfXPEYQSm+KuaQVAa0FpWDGTs4CbKEpYWNk&#10;cEzghQctpOaHlX6vL1nSkCWIzD43sEkVCHE656yAuohJGIrIlrO8E2f3yxlYmGly9g2Cum/mhE75&#10;mo8P+dyys+bfYP3n0RXyFmBozJiH9SiJI6PvO7S3G8a+l3Vx5aKciwLKmhNsbiivlQEMZisiZ/Vk&#10;8iwLf3IEkEOKEjdVAwzM5uuto8S/VD+J1UXb43I4oLvc4EHwxCC1xj60luAeWBjAzPM6WeHZueWP&#10;z7JcfM7YfpPPqVJMPscC/t2vX+burayQtz4TPj8FFnPw8fr95wBaKOEBK7kDFwUHl/Bb6xWiAkjk&#10;ASn4TtUXYZmsuuG9lAt3DHUXe1AiOMdgzeRDTGjPNwxtj/56Q3u+YvfhGeunPcI//PyXOH77LWLX&#10;IfUR4zACpLUHFOGMKWEkhmsWWD49Iex36BqJamdIsKMNgpmlQMejSWMbBmdOoTJZZaXGADjSqppO&#10;gwRNeD9GimRCyia30rjugIV+IKZQES5p1KY/xxP6yxX94Yyx7+EAPG+32O/2WC1XWvhKEJ14Jij7&#10;mOTWwhQNbc4PzpzxbeRFToI0zVrj8uugrhzVjLNGV8iDiZBUJc3+Vy5gT8SeEryXzAIeI8a2x3C5&#10;YbyNePn5R3TdiHboMQRCeNqheX5C8+EJ4WkDXi0Rg0PyhFFBBDkCJ0KKUSqtVqzLgmyt6NZ830ya&#10;rBHU4qJWGRgQ1I9JATghl4kHW70L1fGWK/SQ6osuyXnBAQ0TAhNcigi6wQJbkKUFusW7JcoCjlHm&#10;DkA0+iIBQk47oSLHjEjUNGdNVxlMMvotWnMtxLwxnwhp68VaKyJpLIYnretC+fm4IgWmUicjT3YF&#10;MMiAhVocE5XCazKcEvRX5p3zWOvFo/z0mLzOGj8Aszh8iu7z5xnMUP3W69+aKCfCs4gcUmS0txbH&#10;W4evjid827YYvAc3ARRcblnPVs7cBABTpQGWeaTZG04ayU9PgsWCCS1KyXsNEk1WYv0Rd9ArVMRn&#10;sFPAr8R+uTGiPZ1x/nhE6gc0gAJkgllgCQY2WX8miHhmWfh8sPA5Z77G+77v423gUCw3v+bF9UU1&#10;kgw8azCmn2erTs31ppaNifsD9/RcnyPijIqirl/I4GKmkNW/gVJsy6zbklmQpOYNqVywirVUyRI2&#10;mSmKGyzTU3mt9Twpz5zURS8p3pfDGd2tx+l0xvb9M8Lf/NVfYx0W8MygUZ6BtAATa5R+JCD5gOV2&#10;jeXTDqNWFJSHklAjeT5WTfpxYZmyUPUCVsACIrAbdX/UFSVtAWpgkZlKtUCGzLjihBV/BSACxzFJ&#10;TMWtw/V0xflwxPV0xtj1WHuP542Ais16jcbKZes1iGzRYwEWpqmn14m+xrWvH2LGhZdS4qVrrRCK&#10;eVczaRvBM2vPCi6Nz5QhmiJut2aIP44SI43AcO1x+XjA6esDbucb4gAMALBaYf3hGesv38E97RDX&#10;AaMnjERAI3EfltFhXgR2pYBStrBkS4RaYmbaUHmtSSmUYH0q5JnrmBFhuQIs5NqsRR0CEdh5jE4A&#10;n9wzIUACjUcwAjMatfZIsSlSs7JYrxyzrm+l+VXgx6lWQBANPqcEs5jHCR6uCeAUESFlo5lSAbls&#10;Nf2FjmxRnP3Nop2CLD0S0mK5V7DiHcKykbkXOZgBBQBY/f+c0VzJw8k+sanXNSJFKE73UE2hZc+p&#10;JjSxdFTnKoOtTe66rA+vd3/oc1lJzQff/ZyDyGMcEs6nCz6+HPHt6YJvDiechh60XmPx/ASvcUHk&#10;hWnGapKSzmHRBaYgqaKMKl7OeBPDiqd5s7SyQ6IkQCS9Nf56Jip+w1B3SALFiNvpUupaJAbUbG2u&#10;WVKJVAvYCe+dzO7Dm9+/lQXoG8/8mVaZ3/yoBdzr/PbXyfyYXp4xmcQJD599zvfnv3ovvn/uaexF&#10;BU4qvsHV/po/6nwWuPpJ2pqbwdlyKfWzhJNZkTtOKVdPZiA3qHQQvuqhgNfunwB4qDIp9Z/6sUff&#10;STyGv3399f/o2OF3/4sfI/Xqg48MsENkloZiTQCtl9h88R60WUn5budgZm5zE3C0MlAFBFhJZUCY&#10;ca1lyFRJAGEIQYLviBCcq4RLgWz1BrGSwiYECvAgRWUWZFUYgwREEVxi9JcrTt98xMtXX+P0zUeM&#10;1xsW5PC0WuPL9x/wvN1JamMTVBjYdk85oy7rY6ouyOIXwcTGHTIIIACSNqr4WgqhOAmacT7Ah4AQ&#10;GonYJakzUsZhwX8WsGauByrz6wgjR8V8SqhWiZBZglXZI956dIcLLh9PePn6gJdvDrhcOnTMiE87&#10;0A/eYfXjL7H60Rfw7/aIqwZDcIhOalXk0uFJU1mr/3gSrg5g8rfCY0iGBVf/ERjsVZMmY47Vwtd/&#10;67rmgk82zzCNnvRUB0tLZgUVEVJHYgRhAGMgwuAhXXm9rrEdFbDLAZDkKisc4GydMsjW7qUTa4Zp&#10;BuZKoYo0ubqWEFJd28VSHqFmcUrI71VTIYzEAAfNpy2LQKU/ylvLaNXWIX/nwWFLKK/pEXK4/05B&#10;iRXtvv6TkPL3LFDSrH4Mrbxr6eJGddoQbhwTbpcW3379Eb/8h1/h47cvuJ4uaG8thm5AGiJSH5H6&#10;MWfHCQRQYOcg1Up1Yg281VURJ8PMa0f5Y0lxxWQMpizNCyjdWWEnSpO48TwDPjFCBLrTBS+/+hXG&#10;8xU+JQ1yLlZKwJQ1zU5HVbdFx3BXwunRmj1cw9fBw901/tFxxhs3+AR2+BS4qD/9nPiP186Zyzpg&#10;utcffT8rsTTdZ7KWdPfduQWDZr/rWcosOCukRW5aTBxXJ3K+gK19oUvbd7meEk8/QWKE//sv/xp/&#10;9Lt/AAevVRTlBv0wIKyWGNKIEQmbzQq0WiBqaWlDNQRgjD14TFg0DTTeI/efyCCjsj7kSdSf4EOu&#10;2mmll21m5utGsBLNJXgyb17S6p4pTb7oQQAz0jCi73u0pwsuhxPa4xkYIpahwXqzxW61wWq5lHGo&#10;9QRc6RBkvT8q075Tj3kViu8qri2EUwBWXSgMFcPJpabJQFthMvVYjDiyPz/fQxYUidEEJ5UeNcWR&#10;vMTNcGSkLqK7nXE9XXB8OeF8PuPWD0hEWO33WD9vkb54Bm+WcJsVuAkYyEvFwsw9kSVZFrn6HgMY&#10;1dRWO0DIFlSF2iPmxQq0kHJ7JZni2RcY0HgFuTJrARrbDVJWmiYCNZKmn2p8zaAuFE+sQECsh4EJ&#10;a6wc7kcAACAASURBVPZYJCcZBKRWhQyItPGXEGumPXMQJDMVEIEc4BFAVNUWINJqmcXaUwam4FG5&#10;S43PnAqZxACnESlqllCjLeM9AQbIK/NltsBM1Fh7VlhweaHNai1nizddr/qx7z9+9XibWReLXIK5&#10;FwvN2xkSM2CgQjSrcZDy+6fTGafjBZfjFe2tk+Ja7NBEII2EcRwxdCfEcIPbdAj7Dfx6Bd94wHOu&#10;bJqnoHqGuqNvYd6FzuS9x1Vf7aiVrRxQPjk4V/4095WtPQ8jrscjhssViFF6MjHnFGO5l81XDXoy&#10;EkJBQPrOQ83/lZiAV5bu+wYWnwq6LBa/f/zjsywd3/m7c0Q+PWoAn89XbaEG5/U8s66ZcbzERb5i&#10;8gOhB2uHoUpTCfIkMDuptEuklmjhX8ySqQRQVQisKALizXdaQRYI/8Of//f4oz/854hjRIrib05J&#10;tOFffv01Wsf40e//Lj78+IeITUCfYkbxTVCNMAEcHHwI6PsOHFMWmraBBNhQ3lxmpaibamXLwxuT&#10;bgOphQ+Rlz3DxVxtWiKroBq6AaeXFxxfXtCfrxi7XiwV251YKZaaUupFkMJNYwTyOlIlMrOcrdLG&#10;uFgSDNHlYkygUrKVCM4Fqcyolhrn1JfOs2yYOcDS59LigfnuBCEcP0qwoNzLwUdCGgbcTje0pxtO&#10;3x5wvbS43FqMjkCbNZrnHcL7d/DPW+DdBoNj9CAMDHitQAiinMfPVLJUbDNYFoQAyalrrHjKimVn&#10;ziRykk9G7AZEpzRABtBIvmT9IKaRL+WLYhVx2V1nG5MZiGRBwnLfBtD6IAxP8r64JyQThXSN6+ci&#10;BZVJJVP2m9sJVgxWgZO08ZbCdcwlcNcES1Y5ucytrK1YdCTFLJVW4MmBGg+E0pE1VWuTZ74y69bu&#10;FLNyGHP6LfLuu0MVoGmVVLNK6iJLjRsxxcYYcbu1OJ2O+PjxBcfDEbdbjziwGhml1bQbE5pYgcSh&#10;k943txvCdo2wW8OvF7m08YiqJwqoMFprMmiMfOICIl0zQpnp+8MaKGZgmq2uyPzUBykNkF0iDHRt&#10;i/Z0RuoHuJhAUSvaZIEDXXDKMRe1Tmsg3brVTuZ89mqyJjNw8kgT/9zjc4X1byLU/792PBrLPNtk&#10;4vpHLdvo7juF7qZKlwFyaxZoRDqp3qm8wbNliRSLM9gh97UiQnIOXiuD1rntTAnOAYkdPKTvVy0n&#10;jfbDf/fnf473+yfEcQSnBE8SNHi9XPC//Lt/h7/99pf46b/4I/zrP/9v8Ed/8id4/vAeHox+GIBo&#10;AZwBiRnDMIglIjRIKWXrRY4b0BmzqGrv/GSDAWYWngmdilEyS/fR3OWTgRTFxA6dVGL1h6eI26XF&#10;4XzG9XjG9XzEcGtBkbFdrfF+94Sn/Q6b5VKZkPpUrWV0tagPicZ+agIgFZ+mAVH9fbmo89bzw5f+&#10;FWaCzYKrYlkzq4/MeTGx5jodziFQAI8DFiSpoLFPaK9XnI4nHF5OuFxuGIeIgQFsN1h/eMLq/XvQ&#10;0xppuUC/8OCFR9RARMtm9XA5ij832KqUtBJ0rDp8DQjugDrnubp7j1n7hNhbNXR7JPiSoQ3YjRNZ&#10;VEQ5nJGfghGp0JjyQ0iMggCTVjekJ4aHVGP0rG4OVtDGol9nEcgFNNVBjXJL2bjeaaI4CDE5pDGC&#10;OYJ1Qh05kOPs2iBonY88bhEyRARKwJgzUoTYKHnAJ+2QWkApgzPjMdCVgQUyjpFnxeyN7+H4LgKI&#10;lAFK+rk0BUzmElJrRSAPjgntrcPpdMbx5YDT6YTr5Sp8ibUVPQM8RsTI4MigRNlvDJbCbP1wRdt2&#10;CLcWi/0Wi90GQS2XySyhiuLF9ywKU8ruPZkqA6dzunt0zLsTixW2rKVlsLFaoBwzYj+gPV/QX6/A&#10;OGpFzpStXyYQAMDlMnq2APoPPXi6h3vzdfDw6wKL7yMe47cX03F/37eAzucAoQmAQJFln/U9BatZ&#10;TvJk0VH+hWmu+T5TEGlWXrOASldky8KEZpUQADiJ1QMgIRAaaeWdXidCShioguOrTuFmDQnPuycQ&#10;pMsnMTCmHs55nG8X/NV/+hv8P3//M/zlL/4e/9v/9R/w0z/8A/zxn/4J/uy//q/wkx//BBwlkt1p&#10;HQVH0l1xHFOeGGt1Po4jYkwIISD4oDET9wF+2bdXm/WqgxQ8kGoxbIyHNEqbpZJY23a4XS64ns+4&#10;nE7otR3xwgfsNmu8f37G824nWkRigGLRdBUQmMCk2f2zkH/ISKRqWgEmSgfkcvyED0EXwCP3N6kw&#10;lVSWtPGX+9RBP7UPlHSenGpN3i2R+hHd5YbzyxHHlyPOlwu6GNE5gn/awu+3WLx7RvO0B1YNBk2h&#10;884DIyNYBQgVdFGtFmI+S1nw1RpwcjIr5al0PLNJzKtdzXd+i4GGfQERZNUrZdGTbrZkqjYY1jbc&#10;sILFD9i9xI9v1y9CgfThLUiUWMzxnXYODCQ1N0YAC+cQWDv9cYKvg2TzyktQp2SREICEEhNT0IKn&#10;BhQd2GlVR2gWgSBb8CwgmgmT/A3bAy4HBwoQ5Jiky+yCxF1nFT713hno2poZ+Kl+ZwX6M5TGYpF7&#10;45zJ3tZZekNIkJqUZC1lkh1T7hXEzGhPV1xOF7x8POBwOKC73RAHVXSsg61dLSVxBya5Zkwpu66C&#10;7qF+7DB0HeL1huG0wmK/g9+ssFg0YOek2RkM9Fk8BWWgRsorLF3ZxlorA/LeVGEojfE4W3nlB7l7&#10;rvG52+mCy+GIse2l/Dc0W4rKtbPOeze95TkeggPmfE4WLhWvKXECNLnW5xyPzvsu330clAm8RXmV&#10;zP3sZ5pfYf71R2v4OUf9vTwenn4+mePamoGC8+vvz0HD/Hu1LJmMhTBROOS3AOgCH4TlOnZa/0iL&#10;TFYghpXWWIpiACr/vXPCqNTPHAiEtu0kkNJ7QDfgbr/HX/zFX+AH//k/4m9+8ff4u7/7OX72s5/h&#10;7/72Z/jyRz/EH/7hH6JrW6QYMQwDAGCxWEifEt04tsnF2iCRqCEELJpmYgqcCk1MJx/3dGJ55lJk&#10;TFJhUkoYhwGxG3B7ueD47UdcjifEcYRLET4xlmGB3XKNL969w267RdM0GMcBMUU4L2XErZ20aXkm&#10;uB8fOuF8z4yNICwWRKwVEvhn/i8LACx1KurBPzalAShzm0RLXYQAsJiIh37AeBtxO1zx8u23Uqq7&#10;G4BFgN9vsdpv0fzgGbxbw+03iE3AmBKipmcunQOGUYAiSaGh0RUjr/i4S7wAMaTWSU24qXwOQN0z&#10;hTV5FfbWNKzeb5QkRbAYRSyoLoFJ8lyYqFgi8rzNfmfwUX7fr2IV18KG7IHoPEYkDEmyUqK6Ihrd&#10;tJLGmnLFRLs2aZxOSpRvm+koD1KAjPNeC6M5JLLS0dZpFfnfBEziYe3CzMW7T1EECkeW/iHQ3i5e&#10;Gq6lXOG2CiK171braDLms2vUVNd5+PmvqWkajYmJVgpUgRl92+N6ueDbrz7icjrjer6ga3vtm6A+&#10;ZAtIh3SBlVLEQg+psuCAAUKCZ6ABg4aE1F/R31qM1xaL/Q5uv0OzXuq+pVzds4oEQQa3bABwysBe&#10;E6615eL+PEmP91BeFyMupxOuJ2kO2KgwyNaSmUYMPKL1CmDcPZOC9HzalOOV2K/PBxbft5Xhu13v&#10;+733o+d4y81hn9fPXIMIA8lvuUUKz1Iaq65p4K98FzlOB0BxvcsFpJIn9BxnVlxSnicbPxktOqha&#10;Cc0+lMw9JpGxxkcEhaoi4FyecUmDJzAcAscoKNlLJ0vWB/Uh4E//9E/xz//kj9F5ws9++XP8H//h&#10;/0R0wI9/8hPElNANA4KmcwHA9XrFerMGqU/xdruh73ssmwaNugKapoH3PheXqRdDfjgLW9syqmxV&#10;MybvA6KlpDHidrvhcrmgO1/RfntCagcgRnhmuAg0PuD9ZocP795hs1khuCBaETmozRsjtLubc5hs&#10;sAnSFCsEYJUc7+hP5t3Gk4M1PUC+0jBKzEImRtHJ8/0yE4Kle+YZyeYxTgljHDCOI9rbDf11wOlX&#10;Z7S3HpfuhsF5uA8bhOctmnd7+N0afrOSzAgnmp2HZOwQA4mjxH7AYoEZQbkOK1CyxmfMMm+Eov2K&#10;Jakq5GUCnJErzFnJa7Bq1zYmQFvPV8ibCjHIGVUJtXyS6oxS7EI6ASZISmYl4MtDkgYy1l5pfQ4i&#10;jE6u6SjBJxFOzFqzwm6rm1pzXHKtDGkC5ybPNSEkRg62co6kwidFkPOI44iUIhy40jDK78m4CbCC&#10;aU4kZ/5o6KPMq2dwkMBP8gSX62Oo1am6+IxVvg0aqH6y+eu3jznTfXgOJD0YxIhjRNf3Umr44xGH&#10;lwPOH0+IQ5Sy8OxACdKeQBlsQxLbJK6GhKiC36rDEpE0nRujgEXSYFnIddr+gtiNiG2PxXaNsFnB&#10;r5cIwSNZDQy1ZBR9rszEXEF6NP5HQsrcnKTXcGpNa28trqcLhrbX+Ce1aGQBpAGYyjtzlU6UoOB8&#10;XcZEeOXYpcnxSkAn8ycoI3/7VXXsu8ZS/FO5Qj51fJdxTIBFRRyvAgyUJXnNsnF33ZlFMNXPRwVw&#10;WJq7dYzOgZ9Att7C5E22LItFViyoEuiZRMghW7qUgycNvGPHCFHjF5gThiHqTdV87z2+/PAFfvDP&#10;foo/xn+Jf/3n/y2ufYsf/fCHuLU3+BByVOnlfMa//Z/+DQDgz/7sz/D7v//7eHr3lKvfmemvCSEX&#10;lsEchdkjEudNMFm+vKmEscZxRHuV4lfn0wnt9YrUjfAj4KKc60FoQoPdeo13z894ftoDJD0AUmSQ&#10;12IiZur3vmy4h3QtAg72bKrp1c8pZVJLoKogP1s++aYF/U3Qbn4x13xqdVKjdknS8cZhxPV6xfF4&#10;xul8Qn8d0V5GwDVw2y1W73YI73fAdgW3boBFwMBKYAz4CcCD9McgwAIqHEmhKYCkoVmK+UFt3PnJ&#10;NHLI+rkWbdnYXMVcqYzFLmbujKgUa2BTvlgqCpRp4sl0MUQz91ENxFYxlOuqlbJ+xptZivPnuhAR&#10;VQAUyX0thyGxzEGC1H5Zkod3uiEtzQAM59Ryg8rFQfZPyuvOKol8CDkOJ8WIYeyAuoT+DGjkObep&#10;4RLvAUgMErgAUCSWrr4orhIDcEUYkg44zgjagPCUJm2+7jcJZzcUV2vz1lGfQ1CXD6S65eV4xsu3&#10;Eqh5vdzQ31rwaEhAaY1JHlvnXwrbabM3daUNKUmpb83cSjFJFWJYOBsDyqcWRBjiDde2w+14wvJp&#10;i837Z/jNRujLuWwNyqNWoWEuW1vpWqOr05fLB1zRGgCnPZM8wP0IHgbcjid0lwvS0MOzXJUngp4q&#10;gV6oPO9L5WVU3fOto4DnOeT89MHGo1/57P/Px1uA41PgwV5bzOCr31XePV1DpT2qgUJ1fjkFRhvZ&#10;LVJZ0aSbsCiWxhuyQyRpcUyuSFevk8wfnVj4NTukZHIDgnQSWCKrAcSRgcUCYb0ELQIW3uGH6x+p&#10;pqnMg/RBOeJv/vIv8T//23+DOCb87//rv8dPfu/38Kf/6l/hX/zLf4l/9nu/CxDgmiB+ziTpUxwj&#10;fNOoQBFGEDmBOEn8hE2RbgxPEkE99gNulxaX4wmX0wXDtUXsxFLholgWvA9IwwiAsd1t8e7dE9ab&#10;Fdgp46GkmkcqvqYqVgKTTVL0E4aaKlNp9MWQDqTkXXaBCDFYmoDxa6qWFVlwTojJrDeVa4Et/Udd&#10;KZ4d+muL/nxDd73hfDrhdL6iHQbE4EHv/1/u3nRJkiRJD/vUzNzjyMirqququ6unZ8FZ7FzADihL&#10;CgkIwUchRUABAZIvwSehkD8oQuFDUBZ/eAlA7i52F8PdmZ2ZvqavyjsOP8yUP1TVzDwiMrt6Zg8h&#10;vSQrMyI83M3N1FQ/vVfwJyeYnZ2iPT0BLRpEr03DiHIdCpdR6pSYExgW62ADnLBSovxras6rDMaq&#10;mRNRaT+u/+oq4OWaxnRrl0ih5GkMdRlXWZ1i02CrwZHRH+8JQjUNK1cvzyF06bKFSAYlvVGkC+rI&#10;jIEZDQMjERowAjkEJy4bypplAokTJ9ONxY0UKwwUVAmteN/AO8kaSjxiHKPENVk5eDJQorpzFaBX&#10;CltJhoRJtpik7wqPBPYQ15/XYGrvpOhYShiTjNT5oDioBBjKNFUaFsziw7pu5d5mRLHA5qLZ6LOa&#10;QmH0wJr94RxSAmKfwN2IzWaHu9s73NzcYH2/Rt8PoqQk43oaW8EJFqDJBiiYETlK8z0U5QbMkgmn&#10;XWwNsCeurAnMEvQZATdGxGFE1w+Imx3as1OE1RLNcoEUpKdSAqT0vNKLxT45BTZAUpOzU8Rhgamc&#10;50CsKkoHTsEsJ3hm9Os1Njc3SLsdXIwSdMeE5ADW6xCEqVvGVsYpKHNte+ioeM/ZbfkN3V48eYeO&#10;fzufwdW9jx3fNl7hqYO5uPiOjORJAFWrc4+PqMoABIQvw+RzZswH6Nnom1Uhs5nN68DGIwsPzUrC&#10;BIAeAxllfsm6T2bLk9JyStlCLNfcj5kRijFYbVVqiRyYKVcd9qSFCEktzUzgpNSmNJrhrH3mCE7d&#10;DqKUasEYGaN8Sczj4tsOsxZwDgMShjhq3r6aiZHgvdTRf3Z5iT/4g/8Af/HTv8Bf/ewX+OijT/Fn&#10;f/TH+L0ffB//+T//L/DBdz5A2zSI4yD+n2EQsBBH3VRlmZ3zygiS5ng7eBDGrsfmfo3Nwwa79Rbb&#10;hw3GXQ8akjS7YtFKDKgQCE3TYLlcYrGcwzVS/yGBc6aDtdy2Zi1lsUTLOSQ5aGCjokmSqHantdZF&#10;qGrNjsyElTgMLYIzsdYCk5SAInMugU5JUKSDLD5HYH1/h839BuvbB2zWa2y6XmICFidozlfwL5+B&#10;Fy3CbAY0AdGhVCDMFG2xHhX3z0ROeUAWQMkVAU8J/vC6ecuSmeGTAgwNfMvnTcFVTrnMRKv3cGUW&#10;6/1mpuCy7fUctRJV6kEO5JQzY1nZPVOluBnkqIVkdEBkaVE8xIiRNNCTCS0ILTktM066wfcFbwEZ&#10;2a+kYxTMoWN2Dp5Cfh2JQGMqGmFtpaAqA0Sf2xiU03Pl+hIkSFHiL4Y4oI/iOgnzhfyEmVhtkkYm&#10;sDEJr6mvpkhII6QseIzJAiXbChlnFCeW7S0WgOO0R0BKYkEYhgG7bYfdeovuZoPd/Rbr+zW22y3i&#10;uJ9CbECA1DCTJuAisezyyFyl6yZ1ETPMbGMaGvS3gQunANc5qY46xAFDL/FcYbPF4uICfjlDaFtw&#10;8NIEDeq3dkAaI3KNehwRuBqgYZ+KTa4IM9aqmxgH9Os1xvUGbhjFxatBOGJo4gIimPP+MRFSHxP/&#10;fx5GfU4lcvkQyD8FC4rw+ts6HgcWAFAsb48ADK7+OLiQKHCls6/xaqNjpWwj8Ecun5UZegTAGN+p&#10;h6Bgs5SGnJxegEj9YYWAsjJQ8epDa0oBGHmsNbWwyURRcl0+g5CgtQKsBgYUlzLUBS0eCcfSGykk&#10;FZSZX9mNSOIFQtvAac61+ASLP4ZZfJRIwIff/S7++b/4L/HRLz/Gz3/2V/jTP/sz/Oxnf4kuRpxe&#10;ngOOsN1tMW9naMihbWeaeufQjxGcIki7rqUY4UAIzoPGhGHbo+t6bO42uL2+wW7TIY0j0hhBkUF1&#10;TqSytnEYEZzDcrHAanWCtm1BZNi9YHjishD7loUDesgLltTtIdqf/bZFrOs0JDr8vlMtxvxkOfZC&#10;I+TZaUO4yPARaEBwI2Nz94D13Rr36zXWXYf7vkMPgFZLzM9OsDw7gztZwGmJdiItc60AUsBQBZUf&#10;OR7z8R37vH7PZnJioq+EYQ5hzfOTJ2b6evr2ZI32RgJonIP81vXZG3Mem/knrcETuBJU1fmV3Mzj&#10;YCvsJOs3MmfBluAQScqKRyKQWtgsasUasQHTWAcGctqsFQJjEq2E2cFRI82DXJTCW9pt0ywYpVe6&#10;VfiodSW5A+lzOoiG7kCgIWLsdth0HSJfg5oWYdaiaedwIaCdNeqmER7gCLlqJihoV1C9C2uAmAok&#10;UgFvtJZrtyoQQErgUQKIBVDs0G232O06rNeiNIybHqlLiGOaBKqZixUwoGoxNRVjNXDBYp3gmKr3&#10;9XsWn5Kzn3QthbvDLGxsWhgAFxNi3GDc7hC3PZrTJdpTtWS0QeK0dK4TWXySdInmVBbdxHbej7pi&#10;1lCPIoAxATGiX2+wuX1A3HWgmKRjs86xCa39H7FcGLBFnqvD42ke8P+V4zifom9+vIz594HK4/aZ&#10;/XkUEPE0kHmKjz4eT1GsyU+5WSqxXUb/BOoSPsaPn1shDHFOU1YehPS4PkUy0jJ4sTg6aL0sRiim&#10;T87BdARR33zwaGYtXPCgfqyKYiXV1CU+wTmPOAw4PTvF7//k9/HDH/4Y/9E/+cf4+NefAt5hcXKC&#10;bhwQiMQUy4Rff/EF/vX//n/iww9/B7/3/e9jdbpCH0cZHAhj32GMjO39BrdfX6F/2CJ2A+IwCnNg&#10;zg17iKeQwAEY44AQ5lidLLBczMUcnMRNItouV/P5tpjbgjRJXSBe5oYot601xGoi3OWFke8XplIT&#10;K5WFY2HEwpgYY9eje9iiv99hc3OH9f0aO07om4B0MkN7doqZdqFzTUAEI/oSvyBNbGQDWSDebxpU&#10;9RjQsN+k/kJL8bSZFQWayhv5wm87AIV9dGSl2OmDVaXgj1yiHqeh/JKyWJ7TfJq1RUAgTMWsndPa&#10;CxbaJ9aygZ1kHjiHhoJa08Q/kUt4o7Rgt+sn1UKT3kysD9Lx1bMX2ooSpMiIiEpf4jYzYF2EK5Fa&#10;ztSyUeKXGEiM1gW4+QKBPO4fHnB7/QbbvoPzDdr5LO95Fxo0jZSkJ+/gg0cIXgK2c9ArlYJd9qwA&#10;oNaSISakGBGjuBhSjNhudhiHHl03YLfZout2GAdxA8UhiltKws5l1AYONFXTjgIiyjk1wIhV7Qhm&#10;BifNGMngAihWi8JDcsxNSjleKIDgolQFHm7v0e926Lcd2u5UMkvmM4TgEF0pYGbAhSpDVVZvnOwT&#10;ouJGcQlCU5ER+4jN7T02d3dIwwiXeFr5txIsmRb0xzbd3wZ8eJs4imP85kkh+Mg19/nQxBrDJXZg&#10;wlePXmj/jW/mhUfHpLy1WLsfu/7eHPDx9zPYqEZ07Hu1pageVs4+4uNzfmxM9VBN7xTjDOcsECbZ&#10;K2R0RQSuOvCy7kEbI5GUpwh50lgRtmpR7DxC26JtW/jgQaMDUgSR+GuHKIFfDIbzBN80iP0AFwih&#10;bfDyvXfxzutXObAKEAHcDSNa5/Fv/vX/hf/+v/sfcHF+ge9973v4gz/4A/zwRz/Cq1cv4UPA9lba&#10;fq9v77C9fQD3UpHOEaFR/1AOPNFJKiWPWWpaNAHz+RzeS5dKmXA9bzLvCjQmMRDlBPOfWQyBb7RO&#10;hRaqAlHWgglasQwmzOVaYkbSSHOmfE1Bh67UgkiS3RK7iH67w+Z2jYerG2zv14jdgJEcxlmD8OwC&#10;85cXoPMT8KJFahqMkTHGESUxiFRYqQmL0xNoeUp09e/6/f3vToR2Tai6NmUKivUCfGT3AdWov4lh&#10;mTDTK+6t1zdHEYo2Tp6y0DpuieEMCvf5j3SfdXkzRpLbDixm6kgikDx5hCTxGF616QwH7LImdNTc&#10;nWnBwBp5zeCBdJ4dY9aGHUODwEjBpPjfDd8dBAyzWBpmvsHCNViQR8sON7e3eFhvcH97B/ZSf4W8&#10;l+yuppEYjcYjeAcXGq1QXyrLyjpwRTtSvGocR4zDiH7oMfaS1TTsBqQUpdDeqNlqOkIPlLgKthgK&#10;5LYE5b3ym+0crgI5mSW1184F5wBfWzNWzpnjSqA8oKZp7WhLgDTGAwEJGLY9dsOIbrfDbLPF/PwM&#10;s9USYd5gIMIoDFVjLCorRcYHCjMUyfgkdUtIf/pdj+3tLYb1Bj6Kq4ZY+BpVaNcpvZhVkPDNoOIJ&#10;RfvJ4yme8U3f2Rd2bwNKjl3n7c57+/HJdw6tF9PfT9+PlNkbFRH2QU59LmU6PpopwnvxbZPPs4qA&#10;8uo4kJjwaCArvcaThe8cATumHNgmIXMdcmaHnIPv9+6X+yVxsVzUQgJQ/7x3mC8XCG0rvSmCRxrS&#10;pCJXZMnX74cejTbdiizlZPpxgJ+JuyCOo1S9BLDre7APSADadoaf/+zn+MXPf4E/+jd/hA9ev8Y/&#10;+sk/wn/6T/8pdtsN4m5Av96igcPcNxJFP2p1PKp1QBStjQFwhCdg1jZo2wBArC1Z0CrCL5qwaSzy&#10;uy6GY0c2EXsnFhsUbdzMs4ZiLUgrC8HKZeDkC5kArfkas0SwY0zo1lusb+5xf3eHrusxDCPGmMCz&#10;Fpi3WLx4Dn95Bnd5gqEVn2/PScJWvEddlrvWlp4i+KeOxywW9U8mMLbXLhO+1SWxoNbH2ARX/z99&#10;0MHLKZ44BEeTZ9C12u/zkD8mu6w9VwmuktCBVDa2c2odkhMiS2O8lqXgVgPR7BuoxYu1dfF0a+Yx&#10;k47NGBADErxFDp4aOPaIPiKNEWkYc4EbpzEaec0pX/JQyyUJaqQILEOL2cUlzmZz3Nze4vb+AXeb&#10;Hfq+l0BP50AuZBqSPaSBynWDQH0MAQpiORDLgHSETYnBMWp9FhNwmsJr8RgwC0XOrM2v67VMKe0B&#10;C56cZyBrYrmw8fExy0UtkGXv5r1Z3dtK3jtIT5rUj1J+vJe0VWxXaFZLuOUCofUTa12uwIvpUdJK&#10;obEyjDQM6O4f0D1swMMo2SMEdQGrPlPRSxYWENp5DGBkgfMtBXv93f2/3/Y7v83xbe5X9vm3BRj6&#10;rQNeV22kvfEUvrd3Ov76nv1tjn2+fghOkK0SQPU0xl8qPaqOPwME1FtNoSxD9C6GP1KSGEEk1gri&#10;jJESQlIRnRkfxD/vGo/FyRK+CSAHNMFjHEZYMKf3Ad2uQxtmCE2LOEZ1GWgaK0rAFjmnGQiEEAIc&#10;BfyTf/Kf4L3n7+Gn//ZP8Rd//lP85U//En/+f/8pztsV/v0f/r4EfkaABkYIPkfXtfMZuq7X/aXY&#10;LgAAIABJREFUJkIpBwc6hprjBQA1jcesbRGCV2HP2KebWmDU085chKe4fTStNIgriCpNttYQJZIW&#10;JV4vL3a5nyFGx2q10DLQfddht+uQNh3uPn+DuOvRDT0iJ/TeIc4bhIszNJdnaF48w9gG9F5N5eS0&#10;vbMQRoxjBjJWwEqpMPuGj81DeV09XEWs+wI6hzkZ8udK0FjBHfBknzNzNd/HDmOMRzZnQZL18MAo&#10;QZg2zn0ry1EQpWtAtPeZWXoyA7fn5FKDwywJZLQg0oHZoUsJkRMaACNIg/2kKHNQcGIuBMCyBxxc&#10;hqSqnWjdGXtPALoDkQe7KNkKqvlLlT2jWVsCu35hGqU+gmS5SE2aFvPQYOYDTuZLtDc3uLtfY7Pd&#10;YhxHxRCUa0Uk1vBg1cZtriSaXJ8rZ2MUTYyzEC1ujmwVtAUlkriMai+yZokk66JbuUHSxPWBYunY&#10;Ax6oPldjREVhe1I/xyhZyrKMizWLS5pGS4aLS4y46xD7EdvNDt18hvnzC/jTE/h5Cwqy7hFVRp7d&#10;1XqHQIoUjSSu3m67wfrmFnHXieuXp1kgpBRCFajVBa+3RyHzWvA8KfSqvVqd9tsKym/rhn3b42ll&#10;6Te7n1gx9nnGsWsdcy+bMHjq+pwBfv0ck7/5GIDhrLya+M+xV0fGNn2fs0zTUe5ZS3DEcsPlXmzy&#10;zcCISVSnihIkxkwtuV4rvIVkPTnMrAoNRvIe7XwO50U78SGA0IEgmwQk1TZjSvmcxIAnIMZRskgA&#10;2JAcpCNhYsmnPT87x49/9GN8+O5r/Oh7P8Avf/BX+OXP/gq/8+GHaCIhdhEE6ZWRYsL19Q0eHh7w&#10;zot3sFgsgJgQ46gpmwX9gyOIEkJo0LYNvNdYCBIBmOquq0cWpUxuWWxSLdcHaWw2xpSZbYUzkFU7&#10;ld/Z/M2SecLMUkdQhRVFII4Dum2H+7s73N7dYbjfgh96LSJEGOEwhgA6XyG8+xzN83PsGo+o2TUO&#10;JDUMkvrCUA2KCoY1Bjt9ump8E/N5ncpZEeURQmY1004A2STDX8byrRhUXszHjsPPcuE1Az17AOP4&#10;JtxD/Dr+nJqsn9daoWMGSJL8ssUqX0tGNpLTqrHSAGtkqYsRncRkBDjJSEhVPAZKajBB9pI8quw1&#10;chnKCYmFAOcd0jBi7Ackjnmj2zgmqYR71hLLp5didkLPi8UCbTvDYr7E17MbfP3113hYrwXAyLdU&#10;a7Y6FpWtL9+4Wru6aaEuG+szsUj6AjyA4sbIKe571okMJEhjJ9LBOWLJcUctG/lGVgelogNUa21l&#10;9M0CYinliSE9d7TZnYEkZ4izj4jbDut+RLPdYXlxhnCyAAWfkUHeU8a3quePEF/1ZrPF5v4ePEpW&#10;E8fRyC9bLcq3pniA9p7s7YGFXeib9t63Ox7b90/xg29yn9T7+Xj8xXFe9ZsfT4/n4FzmLHNsjG/D&#10;/54K+py8NxnR9Nz96xh4Jzq8Vjl/D5DS/vULODE5qupE4Y8ELf5HiJqRGSKkp4bFBJhG4pqA0Ab4&#10;4DEkqdo4m7XodzuJHOcE56WXSErKrKoBM2fFJWu4rFrTOI643XbYXN1hc3OPhgJ+93f+Hj58+R5a&#10;H4AxoYGXlC7n0A09/uTP/i3++M/+BC/feYUPP/wQ3/3uh3h28Uz8oFEyBdLQIzQSud2EgJOThSyy&#10;8wBr/4Zcoczw2T7Mny50SmKlaZoZfNNgHKet5GmfawAg5zDGUQFG0TUcOSARKDHGfsSw7aVNtLY+&#10;H/pemisxgODRccI4b9G+c47Zu89Az87QNw5RK/l5pmyxiWBExwCsXGuaPBntUZAh5IkxoiK2eg6O&#10;gQtzI2SBXAdHEpf4FdX+jsZB7Jkwyv0Lgyubozp3P7d88pxlE1EmusMYk8nGT+axhqY+qOZstKwW&#10;BSv17cgXIQkVtqQaKCckStLiXQFIVHAxcIIHY+YIDYDgZK0DSGtklBRPB0ltFDMi5cwFIzWCpGw7&#10;SMZVGqSrMcckhc4YgAlBVEIWgPdub95YXVYOoQlYhQZN02K1XOD6+hrX17e4X6/BMYJ8ABO0mig0&#10;SwqlnktiMGJlMSiaUg760rnd1xItbdRcTwcAIUnOirk/Ehu4kLVIarU0Zjj5rj0wW4Arw+KCJkJa&#10;FYQ8DgBAkntnxVSvkbvkQtNXxeXV3z9gGDoM6y1mZyvMz0/RnMzhg8OYpO5FQgSRtAMQniJWz26z&#10;xd3NNYbtFhhHfdhqnTKwoDzgzMOoslTWe5mRX0+PvDjVlip+eTsesxAcOw5ir/aE2YEJ/1tc+6lz&#10;9uMX9q87PX/fcXD8OAZc9j87OrYs70yoq4yYcqqDv0s37T0+rb9p8vrtxmN3nvwtSKGyfOytmcqq&#10;DGDJLLxlXpmAxptlL2kMmnzfNw4n50uEZjFDv+ulMpz3AByocWhnLVwT9OYJjhzmbQOOowZjTSv5&#10;1dqJEbdFLueHM+TDAkq6vsf9/T14N2DmG6xOTpCGCB5GTeUTjW3Xd/jy66/ws1/+HL/67BP8+c9/&#10;iueXz/DBq/fxD7//Y/z97/4OaIySjZKkgM5sMcdiuRChG0cwGMF5qXWhTdrqNbYaAWCAEuUFAISJ&#10;B202llKEVYHKC1UJL2ZoMTL12TJyWWJih9RHbNYbPNw+4P72FtvNDrEfxQ+tY4pE6B1jbAJmLy4w&#10;e/c56EICN2OSFCFiCQLLz2GpcGTCeYpUxWqyh/onCzcl0KdQtMUq7Lsc8iac5Jty/mXpgcVHb/O4&#10;B16y9kv558CPuAcK7ZX0eZiODewmm/TwmG50SQWt56W8cCjrLiW/pyMzRpIISA55rRKAAYygUCUC&#10;aAloIUIpcKlXYDVaSAOnTOPnPNesaWDQio4CT6RZWUIaIWV+UQSraBdW8lekR34qxTAmOAFgPmvR&#10;NJdYzOdYLk7w5uoKNzd32Gx3orkTSQZWZEQecvbYGCMS9gA4I782oZ9XbQ8EAMig4phlQkDG1HJR&#10;rl3cI3zwXtmfbPRlKewZRNqoSlqdVd60shV1z5sS90CFUpkRosOw6TF0A+K2Q9z1mF+coj1dYjZr&#10;EQnoGHmeYG6nYcB2vcZuvQYPYynIpopAYbf7e+FQ2JRP6t97R2YAvHe6mb8fF6DfZBn4TTX13/So&#10;QYW9rhWKg5irv/GD9n5Xf+8tmFkF8mvsr/DxS5uC8yivthOnDKq69yFfzadXJSqEdwi1Ry1J4b3X&#10;2kksVlpieE9YnJ7gxcvnePn+S4R3338XV2+usNt2GNMozCoEzJZzzd9Wny0Y5CRIMsYRwxjBSKVg&#10;FPN03FBGdbCQ8oZvPJanS/TbE+zSA8Z+BEeJmE6p+JXiOKKdtfiHP/kJwnyGz774HJ9++il+9ctf&#10;4c1XX+Gds0t87/V34MaEWWjRjz3CLOD09BSukW6sliY6shTbcmrJqOVfqoQPoZi7JVJfYi6SAgVp&#10;PSLgAYxsjs5aPDMoGZAS5oMIsVJc3WK72WD7sMX2YY0UgYYcnAtgiNtmdITOAe5sgfbdZ/DPz7AL&#10;ugbQtDXVlizlVO7PqoxMEfukYNSEkA8RspHcY8cxjeEA4StaZ+lNLhUYFVTyhKDd5HoSy8Kw8vPM&#10;hwwhCx2SXhmkGnNd8jkcaEcWb3NYFO1Ai7KRSHEHGT9SmUcGOEXY1YwJO5AEIqrAZ0a23sCJZckx&#10;IcIhshZdAuWCN5aWKIK+xGPksam0Mzhmlgwzbki9lQA4BXoxSW2MJP0zXLVWBji4Qv6CiSz+g2GW&#10;ydVqidlshsVCUlSvr26x3mzQjdobiATUxpgQ01jakVdzay4GCwYvc14FrtYggNXkagGaBjayNeMQ&#10;TAhIMbDhDF3ATLcAyvUyMcVi0QDn4muReM/NBuWhps1ZCrLOG0vQbumYCgQWWuD1Ft0wIG13SNtT&#10;hLMT+NUcTZDChGMc4MjBM6Hf7rC9ucG43oBGyfpysKJzyuiZqv093ef1vq55b71l5TzotWzi8P+L&#10;45hlZP+w2KOnn3kijY9+XuTDU+e9xeX/mo6pImm3KDDlmHvl+NCyliH1m1QeJqUpqeipFa716j44&#10;zOYznF9c4NX77+Hley+xOj9FePX6FRarBa7eXOHh7gHd0IMd0Mwa+OCrugyy6YN3mLctgB4pjsiF&#10;b5VJwoTdhBkiP6r8ZpAnLFYn8CDckcf91Q2GKIWvyoaSghzMjA9ev8bLV69wc3ODr776Cp/86le4&#10;u73Fey9foQ0B49BhGEapEOYb7PoO267D4vQETMBWO7i6ppWx1VHggHaAU+suEaDdFHMJa9gmnUbH&#10;61NnJuQIsOZVBGDoeqkmupHmQw+39xj7AcSExjXCnCIkU4QIyTuMnsDLFv5iBbo4wTAL6OMAx6LJ&#10;CLAwrQqZWXpWgXGg1+vgq/Fm0jvm+ngi4pKZc9O54oPbM0FGAHCakkt5vslKL1Nl5k6VadtEu2py&#10;ThfEaydZcmKu8170f2bpHhkTq1D1cJ5gAQv7GosVYRIL3SGwKCca0FFmrgGfZGi+vj7bBpZ4GlaA&#10;l9sSZyWh1JCxmkqVmMsZJV6qLhW6yotlwFHXzWiXxJxOTmNvHAT0uiTrFAeNfUh5b8JKUWcytmtN&#10;pJDOgUPTBpydncE3DS4vL/Hll1/hqzdXWK/XsNLl0dxiFYMr1gKtiquBp1kTOlKfQsCFVdwsdGV0&#10;knLzuPo7qtln4JImAATANBW1omWbUHGDSWClAD85PFd7m21VOGt04ipxCkYM+Iilt9Hgq7TtkfoR&#10;m10Hvr/H7PkZ2otT+DYAYHhmhDgg3j2ge3MD3u7gUsp80BzWpsAg89e8ZdSVVNDE09nY1ZefOO8x&#10;If1NVom/3niHb3dw3o+Pf/5NoyuPNwX4dkxdPY9ZjX774zedR5FDsidqHli7VkrsRMX7Te3bu29R&#10;9YUfJ2IklqKXTdvg4tkFXr33Ci9evMDpxTmaeSvVf5tFwLP2GZarBW6ubnB1c4MhJjSaJQKopi7q&#10;qGQlNA0ARuoZXZRSu9KgqyI8VUIPllqFRIwRPjjMT5cqMBwebu4Rdz1SBIZxRBpHLRzDoMiYe49n&#10;p6d4eXGBv/+dD/Fw+4B3zi/Qd708Pif44OGbgP/lD/8Qb26v8bs/+D384Mc/xOvvfICmaUDM2lHR&#10;LDKmMarQI1ImVLS6GCPiOAJOrRiErIUB0llV9jyDR8DDod/16LsOm4c1bm/usF2vEYcIHhW0AFJO&#10;NbN2uVkEEBuH5fMLzJ5fAm1AzyMiErwLoomSkXwlHIHMUNIeTVodCJdfy6o4VWEMQLKO5jGS3neD&#10;GKjYL0bltYgTQUDT0PXSCr4fpdncdqeCnqX3RdYoZQ5DE+CcQyAnFSODR5i1CEGyG5pZC+cgKdLw&#10;KrCtUqc20snkVj+N+JGeChijTL46RxX/lUBDzr1RKjGlX0pKh9NPbe9AwR8TIVJCr1p0YuQYjEBA&#10;iBI46qm2IVkYJXIatT5Avm5243kCKImrzJOCDLNicLZwHSw0GzBVjkkQMcuAD4TT0yVOVieYzaSk&#10;/tdv3uDm7hab7U6yVbyXayR9/kq4GyjjXBZbaecIuJi6tfQnFcuFze6BpUPPlfsYTe0J0kr4EOTc&#10;ZHRAqXys218AjlqpLBg8g3rbP6rXsS015XmzAlkpJowpYug7dN0W7XYj8RjLBZbNDHHbI17dIt7c&#10;w/UjGq7tbIVD5B4i1Wf8ltaHAj5sHr798Tbujr+p46kYiOlRKw1PfvzU3fCYBWPfCsBc85npxd/W&#10;YvDkSCo3xtuen5Uj5SGOhD/IU03dIQVa1N/XTE+V196KRWoe+Wze4uzyDM/feY4X777Es+fPMV/O&#10;NW1eoHBgYvg2YNWuEGYNZqslHtYbLFfL7FexgYK0L4D3ALUYGRh4J1q3CcpcygsZYNQHg5GQpP5F&#10;BNgx2tUCl01AaGe4vbrC3dUNvAn8xOI2AIAhYg4HlwDvAlbP3slafEriwfTO4f7hAf/r//G/4Ref&#10;fYznf/JHOH/xDD/64Q/xD370I/z4936AZ6dn4GieVWPKyqDsQZnV9y6a+jAM8I0GwVQL451X94hU&#10;70t9xHa7w82bG9zf3yP2A4ZhkKCtKEzAew8wUKfiSbouMBAjzQIWzy4wO1uhA4AUERxpq1vL1tnT&#10;YPZob58MM4HVBGSbVZ/TZOBjJHwsJiMToP54kiDDcYjYbrfYrjdYr9fotzuMvQQdjt2gpmzWBnDT&#10;O1qsjtMCTcE7hFbTgAOhnc0wm7WYr04wX8wR2gDngsS0WPomFWFVT4IjC7J6nEnlVibElcDQNwml&#10;TXwuJmHXkXoEUc9VZTavhtEbEYHJSVR1YkQAvVa1axxjQYQmaU0FNo2Z7fbCJLiyitizaFtk57Qr&#10;L7NY0WJEGgdEaYKhAn4qkGqGJDfi6qk4W+IIwNnZCrP5HGdnK3z59Vf4/IsvcHt3j7EftUaERqbY&#10;uGzCIcBBbm2WCEEE9lvmdd+SIYLb+obYMY2l4HzPXO+iAhc1GLGqppN4DflDaERjKNjWzMAbuNRq&#10;YQCaQgoFNLZ/QA6cgJhGgKXLqXcARyBhxPgwYt316O4e4C4vsVydYrh9QP/mBm7bwzNJ/BjKHDiy&#10;IlmWdo9an5xSYbWuj9H4byfufnOt+jf9/j7veVuAsW9Z+PZuEZvdbxxhPu8As39LgEF4ZH72hiKK&#10;sf69Z4F+7LqTe+wBjCwTmHNXb0Ak4xgjvHfwjcN8OcfLVy/wwYcf4PnLdzBbzKUgIUT2E3kQAP9f&#10;/1f/2X8reyPCNx6LxQKz2Rztcg43axCVyTgApB3XiCDtWTVaNOXNuf9wWVTnI5EUt3GNFHtKqomH&#10;tkHTNAitdIYcx0GsBSkhwIEiA0PEzAfQKCVy502Dse/LpnIEeMLsZIFnL56jiyN+9ekn+MWvfomf&#10;/+IX+OSTT/DdDz7Ehx98MKmcx1Segcjlpk9kLhFlSGK1KMDJWWxAihh2HTbrDTZ3a1z9+is8XN+j&#10;3+zAaq2gpCW9LWNEe5MwSnvq5AidJ7gXZ1i99wJ+1kgBMtIeDZWAI566nCyA0JqRqXzNv51ewyKE&#10;iyCp6NWQ7tOFKKYEq9ezIM84RHTrDuu7Na6+vsLV11e4u77F9n6Dftdj3A1ivRkha5pk8E4UR8mW&#10;gYdLkl6btCT00A3YbXvstjupjrjtsF1vsd1ssOs6xDHCuaDVWEW1s80BlA1OSvz23nTj65ZjQ/cm&#10;zPUc2YEScKmIXuSZCno9J2VwUV/LhAOp+0TOkCwfYHSEwRFGJDiNucg1LmpNhEv8hC2g4ZcsYOwz&#10;rTljJtBpXnzp85LHZ5YSYkAzPqDQr7qTgEjvMJ/PcHKyQNNIka0YxTJlc4uYNINDK2QkuV7u92Hl&#10;vM1to6m5++Ai9wkxjVtBy77VQy6V9PMp+MgMykDtBJRk3UnWKyHHNGUKokyq8gPkVPQcwcJcUkz1&#10;R8OzYK3fY0rwADCOSLsO2A3ghy22b26wu76DG0ap7OoK/UkVzhLDZRVt6j2eh8nmop4eNTguqazf&#10;7shz9VsAi6eE4GNC+G3f+6Z75tff/I29s2jv9+F1pzFrT4/5WGOy/Zg3OvJZcWsg854aFNTuDYfp&#10;2OprONLMxeqzmjdI4zzksv+JE8gRlidLPH/xDt7/4H189+99iBfvvsR8uUCiBPKAC075gFgnQ4xR&#10;BKcnWDjybDGXoi/jCHiSvgLeAexybjY7wsy3YC9FYrquExDinPjAq8NeJWW+jhxiFN+p8w6cCJET&#10;/DzgtD3HfDlHmDfY3a2xfnONoRuwoEaEf0zwor4jdr24F1JC00omBQh4+e4r/OAnP8Z/+I//Y/y7&#10;n/0l/vhP/wT/7v/5KTgBz55d6pikvbvTyHdyDsE7eOfBPIDTPikJx2JU6aVJGqR16y3ub+9xd3eH&#10;ft0BPSMNav4lEqGpZimpLgho0AciS4obnLb09sDi8gy0aDFou+y2aTGmhIHHqgpq2YysS2fuENO8&#10;7KgBxcScpwLMtN59IgcwCb+wqxJUI2C7vqRC7rYSW3L95Q367YDddivgCECQkGKkGJEtRcnAjMYD&#10;qBpYuvIB1qlXgJ8Q/ZgihmHEdtuB7gm+8WjnMyxOTtDOG6zOTxBaLxlPTuILYg7glRgFtmfjsgHN&#10;apM122kvWSkSA0ayYk8mchOQgwlrTcnANptSXFch0HVzThtdGU0yXBKLRkMejdZXCCSgK9cAqUz0&#10;BRColg8RZlkQEcH7BkQO7APGQToZI8UitO0SpMyFaiCKbEEgteY4EFxwODlZ4nXzHs7Pz/DlV1/h&#10;1599gdvbe8QUq26lheLEBWFzwjqfXAQ+GTBAlWpatFA5T10qmIIEzvPsppYpRvltilB+OeVVhGIZ&#10;AEQZgqMcfFtiKzRzytIH9TtiK3EqATQVOdn7atnqIhovDD7d3OMu3gIxIYxJxg6JrQJxdgORsyVO&#10;Kny41K1QwFlKjz1yUD0Zx4+3cXt8Y8yFcYrqtG/SVx5zTxqgPjifjB8dPvMBDzsY76FwPzqmyUmF&#10;WdTZT7WrZh9PlLvw/gdvZwzZ+8r+PNSWi/xetqzX4LQ0EDRlop4A2ruBVzloYHS2mOPs4hTvvv8u&#10;3nv9Ls4uzjBbtAhNAGtbdmK1xENkugMhTLRUFsZJTpHtMMIlgmMPD60CSaSbReRRGwKwnIOI0HU7&#10;xDhkzdChxGFopQBYRz9JNXUiMEg1dzAoALRscfH6HQznp/DBYf3VNXbbEZ6SFJfiCEAC+DyR+FiT&#10;pJc2TYPFco6T1QLnz/49vPf6PfzkH/wYv/78M3S7Hu+9einZLnFE0zZIzFhvN/jDf/WvMFsu8KPv&#10;/wAvLy+xWi7R9z04Mbz3YmHhYu1IQ0K37qRGxfUdNg9bDMMIJIYjDWU0KUZqpUFxAbBOIgOg4LAb&#10;B2DWIrUB87Mz9FDC9R6D1eaAV9BDRklFu60YIjFp0yWxtuR96rzWKIgqdClXTmU1hyUGgi/MuhS6&#10;VnM7OQ2DLxmn42bAw80tbq6vsX7Yot9I3EzSdOJs7am0TmHQ2GP8VP62jaJAxLRvBkk9BCtZODKG&#10;ISFuI7q7DhSA+zcznJyf4OzyAovlEj54OHIaU1M0XhAXWqeULQvW/YO5WILkDYJo8q4AB8h7UJq3&#10;s42fmRXDQID4LU1gkAB2QhYiCUCv6zoCGCMhEtASYQ4n5ketY2ExGLb4lkXAFEUA63ikL4VTC5hs&#10;/uQYHEfEOCCmiMgM4pTBg1VbzYCyZtBVNKEnYN62aEJA2za4ODvDZ599ji+/+Aq3t7cACN4FcQ3G&#10;hNA0kslSkByIpbuyBVVncsjAwlwcyHSTNXEFjKTgx+pc5AW0Y+/vDCz4uEDO8RRgyczJWiKym6Ks&#10;ti16YdRJ6TTHJpEAUJk+UTjASWpZqMvUZVqRHwk+rJ6FdWZqNMi2bSorsc1ZHt10Po4J1keF+/Td&#10;yZ+PulwY2fo5OaOasz39U1LI9Qsi52rrypE14iKj6+fJylK1z2oZ/xioqMGMFSM8fDqqnkEdZQZy&#10;Mh+urcYGPKwmkNrBLDAch0CIjHlYuwpD26Zs2diiVMQ0hbGKmJBkKZ0clwg+aeM9Kin5CaQmMYLU&#10;9UkgSEyl1x4VTMDq7BTvvn4Pr16/h/N3LrE8XUpZBq0bRWDtB6QP7aCgOCHsB8mQI8XYBMSknR0h&#10;tR9CALwKLGX2noAQPGbzBkQJfT8gjloDA2bytwg49TpzMRLLghtTFG0kBcA3M/jgcUkvMJu1WF/d&#10;YbhdY7Pr0XrLs0eJnUgJ3nm0TYPZrJUNHiMa5/Deixd45+ICY9dLSukYEdoG/TCgmbX46JNP8D/+&#10;z/8TfGjw/e/9Lv7FP/tnmM1biZ5X85MFxUUGNusO27sNHm4e0K07xO0AjkBgKYoTtaiI7sAMnEbm&#10;zAiQEjy7TJQRDHZAmM/g2wYDy0JL9YMiaJnr/OVCmLYhAWgxn8r1YtqdE+YrFQM1Ut8YujJpZs6B&#10;ixLY57N7ISUgxREz3yCOA/rdgGG7w/p2jfvrG2we1uBRuB1BIu1tIyZlivwIM9fT8gYFKh5amfTY&#10;HpahDbOUNsFIwwgmxraP6Lc9uocOy7NTrM5OMFsu4Bon8RLOHtiByCwOSXm1pfdOStlU46s3sjFr&#10;G195u4CIIoitZkI5pmsq6+cQteJlYonHsNoZZvlwJG4oYYilyierYBJQq2zXQBsBYKEl5zyII6Je&#10;i+OQgz1TTV9TyVCECZEwTH1OgpT8PVnMsWhl/y0XC3z66ae4vr7BMPQgiDaUork3kHuEWOyRuR3r&#10;niBCB6XSZwUxKoFbUk9lvxVrzIEArIFFtbol8FSES7YE2btqybT041oaGaCQKRfrDiW1OlgPFgUG&#10;OTYkQRSAjLDFyuPU4WLAJ2cFcRl7BhFl8PnPPDcowpkngvrweDtgUV3/iYvlactaT31+CRrfu2N+&#10;ppqP6cMc8As+Qp4HYzoCfJ6cg8mHj4AmKs9kVpPJWuRbKwhRq2W2PukaUuWyNW6SrSBmCSA3ua7R&#10;GMwKoW4zC1PgTF/C552m0kvBNofIpBZXrTbrxSrniMVSwZBCfswAR8wWC1w+f4ZXH7yPl6/fxdmz&#10;C7hZUDk2Qh3L2plcDmeVcSGyPBxOXjFjEpBL7kZWBMYePrTyYIqk4AhoPIKbowlBCkPFJIwEyEV3&#10;oGiu5PbbSpWYDZCT1u9JWrkvz06xmC1wcrLC7ewK6+tbDNsObpSGKs5JdgZBgqDmTYP5fI4QgmSG&#10;MECc4JJoWeSQWzeHEDDGiJgiXr9+jZ/97Of4/Ne/RqvpqnlTkhQP6fseQ9/j5qtbPFzfY7fewSWP&#10;VqfRzOJsi23ClKVrpqStMSwX0Zm2moTh9wBWq1XuzyIrRkc3ylHiz+uoNOQJPlStcSGmZu+mqZjm&#10;b7dQuXEYEXzQ5nQC4ACZZ0ce3cMOD3f3uLu+w+5hrQWDenCMuhbCTPeZ1lOa4t6JB09mqN0ekNTq&#10;MtEa9LwUE8aHLfrtDuv1BtuHFZbnK5ycrTA/WeRA5ZRKppMF4WXXBvAIjzHBUz43x02Ffmx6AAAg&#10;AElEQVTO4ti3Veqz7/usJ/NTCRGCFOESWlIaQcLIQA9GQ4QAwBPpfCODI7GsZeNScXWgyiJyCviT&#10;B7HWC0kj4khIaQRiKXQ2xReU94SBIjMHO3UtMhEuLy6wXC5xfnGOX3/2OT7//Evc390jjmLSTwYK&#10;hCCLlpcSxsiZPxSGWf3YmhsjVWA4ncpKIGdxzOW1qc1U004V1GnFtfafH5UgyZYhE/iVaNc9T/rb&#10;OqWV9dZ75ZzkLJFB9TgrmjmgE+y//XY84m/6yJlsVP2tr0k3Fuc504/YQIW8TlRDo/L9fI/DtyaH&#10;GDWPz5PdMK/EvuVAf2qh+cgwKojE+aGlQR0jed3nBjDztQVAWC2nHE8HlTcslKjGsgI+lK9I8gDl&#10;XkIpAzPN/lMX3pgEoLjgkbwHeS9NLYMDggMFsZh6ZtAwAt0AnxjLtsXlxRleffAK77z7CufvXMLP&#10;W0lxt3ihamZHDSK39aOsyFTgog70Mv+zETjAqrUn1ZDERO/IA8HBk4MnwkgJnqR3Rj8MxYoBySYR&#10;bY4OlrwQC02YjqS3OYTlHCdNgG9bzFdLXH/+FcZth76XNtaevEZkE9oQ0DTqa08J3jupr+CjNFZS&#10;PzAgmm9KCb/7ve/hv/mX/xI///lfoQ0B52creO8xDAMYDO88Gu/V3cAY44CRozA6p1noicEcM50x&#10;QaskCg+xgEvTuJgZVos9MQNBzPbL1QmcI+xndtR/PxX4JIwtwTkJugvBI8aopZXlnDrDw76XkTgD&#10;jgIcO/CofWc1G2boJJDy9uoa97f3eLi9R+wGeIh7SgpJSdAvH2GM++89xhAPNKlqnACk5DRILRY4&#10;fI4xZWExbjvcdAMe1g84213i4tkFVucrLYSmwMsi86DBmt8Y0Lo3Pv1f/N75jaPP9Y3+agXhDAGk&#10;rMxmBGMglvLhBDREaKFt3TUjgXIdFAmQTUAR1CSaCiOBSZr5iS7h4ALAUV1jIyFBy07DrBU8neNH&#10;wJN9HpwHZjO8ePEOVicrnJ+f4+OPP8WXX36FbtchRUVAlUhO2qxMXrvMJ+pYijx/VP5mQF0h9b7Y&#10;zyaohLo9D1TI7ctuFfDQeT++WoWHHf28so6Y4LQCb3nu9LOq6ImAWrZxHd8/dk868v5verxd9sXb&#10;HQyUmCZgMtASSFq7cGrBpICEtCjZZJCoceIB+C/BjHIfK9W/Pw6VZgUAwWixjNEnzg3j9OLV09V/&#10;KUiqlB5A9tko6FJ5EqsCgux63Sc8MhhhQeLaGDErYwZYYckFPvM7wRSuKPrBA00A+wC0LdA2wKwF&#10;2oDUCMAg79B6BxcT+H6DdL/FIrR4/52X+PD9V3j+4gLNsoGbNZrwEOE4SbNDACNzLhxnrmPHxVUO&#10;SlPLRZ4f1UZqE6QgaqkBkXqGSx7gAEeanqpMx3mHsJghBA/vHHpNwxRzScQkQztrDdPXSBadT6I5&#10;E0BNQDhfYTVvQbMG6zc32L65xbDp0LgExw4uOMzmLZogwIKY4cnB+QRCAKJUxGs8Ac5ht+vgvMPJ&#10;bI7f+c6H+OC91+i7nSxkTAhmuuIiuNq2wfnFGRCB+4ERe63W6KR0es6c0UYEEm9CpScCGZx3YBIj&#10;6AgGk7Ryny3n2Zf2Fjr+0cOYrPfi0rCiV7Kuso6TwM7KgkEMBB9ASbRlTpKKu1tvcH11hdvrO+y2&#10;O8QhIg1R/fmFVoicBG1WggFGRXz4XhlD/QB72sResKkxJqMRUBF64i5zqjnKLEaM2A0D0sgYtlt0&#10;2wucnJ1gcXIC51sMaUTU/jnOusZOzOLT45jZWLSd8lRcjWk/M+HoNWuBrbe2gk5RH3hgQgAjgtAy&#10;5DckHgPs4NWsaf5qTQ4rFTMBJBKXHau/lJwEipK2MnTkpC9KGgV0JfHHMix4TEHo3rPn2AiCuBCY&#10;EZzD6eoE81mL1fIEpycn+PyLL3F1dYNhHEVIWN+fOAIEDVhGIYi9Kp02jwYM8vu6pwyZWSmPyRpy&#10;mevaQrXvIpFTj4GTw3cO5wGQgIpKGzdAw0U6ToBFYsufzc+W78OVID5yz7x36+d9zK/xxDEFjHzs&#10;kd/uOtDtk1/A/qsNRTr3BXxNh0ylXArKd8yraOdnQX3w/ULz+XbGdlWuJZ7KH/vbKQ1PAjSpTEcp&#10;XAiDoQV8ULlYI5tNx1rmVgqjuQqvGJAt44hEiAY0vAEOsQxCfyh4uOARQiOdy5sA5wMoOHATkJoA&#10;+IAUPFKQ1zE4xECZnwQAs8RoF3MsLxPmq3Ncvvs+Li4vMPMJzIO4KD001gsYOcIsUATpjSRNTy2Q&#10;vKDAAi5ErT3caPafErloemLFSCnBJwY1DB88GpIyzyMYRI0UBQoBfd+hH6L0HeBYWTCmh93ZUu1E&#10;85IIenIEahycn+F89gKLxQKbxQLbq1vE9Q7dtscszEp9DpZ4ApsI7x3QeADSTXUcBjRNg5TUVw/A&#10;g3B2coo09uCUcroOVOswc+pqdSqa3gis79cYe7VikJYIV2Jy+r1czFnnshCSBXkykgPa5QLtbI5I&#10;inOrDZDX5IjWuP85V03FjPmatcIRmZU2a1TWztZD/HHi6AfGLmKzvsft9R0ebu+wXW8wdtKJ1hFJ&#10;ijCJ5j9pZFZF+U8Ewt57hfT2UHxlliai0owpX5/L30QTQe5AIC+uEfFLCsilFDFuOtwPA9YPG5xe&#10;nuHZ82dYni7hQwunnSstSKnSeQ7neP81Tz/b/9a+5eWpI2veBICiagdyYfHGO8SkoIOVSaqmzyAE&#10;tW4Ql96uFpzF6teXLBh1MXBJkZYYFpLGbMmra1P6CAlDNiGZCribHCIsEicFth4pMbxr8e6rFzg5&#10;WeDlyxf4xS8/wps3V7i/f5AgaIgPOqWEcRgA8iXw2YSzgoFMT1S/b6hOdxYX4FEfll1S/N3IoHv6&#10;FIewfnItmq6xmb8n76E6h5DTbMncKQaIUpLKr7oP2RQAHO6Tx6gnB3Lr/weg6m/5yNrrBIEZ1ysH&#10;VcDDrOlycAYP+es66VmxAEzXr65lp2nG2R5et/vYeZP7VeNiYgl4np4EhUMw137BwPWNtK4JA1r2&#10;Qc/jbJUZGSUPjRycl2QJ5zVmzzlxZTQevhHw4NoA3zQgr0CibST+MXhQ8Ao8COylynP0AUxOs9EE&#10;sIwEjFVmmksRPQNLH+DhsV4s8WVDaDHihfeYuQbE0kYAUeIzxE2rJSkghQstFkzxlM5UBS6M4GtN&#10;y4K16nLErEgrRQanJOmfaQSlFj40khcLBnlCcNJV1XuC8wO6vpeeJAZUdCA8QXalZkK2AhByoGlS&#10;QbJ8do6T5QK71QrrN9dYX92A5i2a1RLJic7GVLRJTw6ukXK7zJXJRlGqJ6lkmKIUxrJ8fQC5twgI&#10;4Cgo8mR1gsY1mC8WuLu5w3a9RRx6gDzAvpg3bdInBK79GCAukaQEdXZ6htl8ga1WcaqDNx830Vak&#10;P9nMCTEOAEIR/JqiB3DOZQYRvHbwRAJSihi3Eev7BwEV9zfYPuykZDmAQAFeLTsxxRyESBC6iWmE&#10;BaQ9Bi7q45g2T1WhJECZtyJloYWSo22/s7neiRvIwcHBS00NIjCV1M3NZo2h69Gtd1idr3B+8QzL&#10;8xP4EGQjuel49gHeY+swARkHJ2luOdVzYv/JVc01ZZyPoPmHjipaYnVvmFwVF2Yi6VFj5fg9kLNj&#10;bDw1aKLJgxkjlflzypTgolq+HFKS2CQLSBQwrt/mYh5WaISURqQ0gpU2nHc4OzvFyckKp6en+PKL&#10;r/DxJ5/gyy++xHa3BUDwBIya1ispmYzqQbNKPImLqcYgfMpZbGQR+LWWCGTwrUyvhNDkC6YSD1ZW&#10;pOzfCc0eg5Plkzw/qar+CTVxZ2CjAbnGF3UOdQYUyE0xUKGvY/vnyHi+Bdb4bWFJnblmF6yFOhtv&#10;PNgk8vwOCS4V6FB2SLlGFtqT92T2nuKV7vCWmS/kt1QYQ98XN4ZcOzektO8ySoYIURmXKqLsJIic&#10;VfAjeLDGQSA4UBPATQNuGqAJIO8xa1r4poXzUpHYwAN5DzifQQQ7yspHrpVEdcxKqahBljECzsqK&#10;g5fEZ05YjxFxfMCm73H9MMO7Z0u8PFni2WyOhW/QJAYoIfGYa/MIADRlQ8IhyPYo+NAtUoK0Dv1w&#10;BiyM9zIkIyNyUh+VWCy80+BAIrSeQLOgJmfA9QO6rhdWyiW4s6w1Y0xikiIzUVtJUWuFTcCIBN96&#10;LJ+fYb6co1nOgJgQ5x6jmiW914j6FJVIPJwGcgLAZrtFCEErKmr2gfYUYeZ8nrkVLGgNLOBpfrJA&#10;aBrAATFFrNNW2zA75OgSMtQqqZ+s85IkYhQDWJCqc5ifrRDmLQi91Maowf4h8H/0cFpxc6wAUtaI&#10;U4LP/kyChwCvGBlD16Hb9bj64hoPt3fYPKylHwtDa4sARElb3ItwiYzcyEsviRjldaoC2eqfYy6Z&#10;+rXbe9CDmAsb/TFwIZgcROrHtz4gjuA0w6F1HhgY65s7bB822D7scNk9x/n5OZq5/8Z6AYn2mKe+&#10;NgvKUYZfr0HWLYtvHspwwcLn5HoG7nkSeGZgdeQq4JiAUc2pzIzAdiZrckzK1yq1NgFzkCeI5Q2w&#10;9EnJ3GDntFw/SQ8cnVMy7spVfIHNhasUFH0mZmkx3rYBl5fnOF2tcHl5jl+ervDRRx/j/v4e4xhz&#10;kBobAKrm0rKFkmqBcv3i4igz42qYhr2lyjRh18yAJU/w9BsTkHCwqgoUJuvD0zPUigpmCXplAxMK&#10;BGrNA4D5OIqL2uhc57kGGfrfkzET3xYt/Bbo4gBY4BCcwywzR0C7xDxoT5dqILXZ3SjLAjMN2BZb&#10;HRXQsD9XKHw1356n+V8RpPFxVOK4qiPm/SOLJBZhVUAlTgDwsndcEKsDNQE0C6C2gZ/N4eYtUlBr&#10;eggSJ+FFRjjSqs/kKuBQWUvUHcw6t2LlyhsiAyVJedYChfZw7DKws5YdACE6wpYThr7DQ9zh6/4e&#10;725P8N2zZ3j/5BQr8nAxiaXTea3PZEqGAAxngfwQI8ABuACmG8UWI0eP60eGgIgZiITEkseOOMLP&#10;WvjgAY0pCESgxiO4GZqmgfce/TBiGKLGVxiYQfZ1SRp4rKwnykBUm4+syLdxcGGOZfsOaEwYWiel&#10;lHXsTpld1DGT9wgQJhm8l+uxMlZ7bpYS3fvBkxJMxjZJgGc0s4Dzy3OE0KC9vcXD/Ra7dQerkWaL&#10;LkzRaeEueU9cIjJIFzza5Uza3A9DYf97gmX/qK1M+T1o8FgVxGnmXwm8dBpcJkCp6zqsH7a4u7vD&#10;+u4e65sN0pDA2preQeGwtIVFbaaGaowp3ztpE7Yi2HMxJDP7ujLPWTutnmef/pLRQL1pDEzoZiIi&#10;Na3LTkpADmwkkjQp0SgkyJe12VmMI+7f3GLoBvTrDqvLE6wuT9R6oWCS5Hs2zCy4ymPk/3P9hicO&#10;s8w9tn6wfZD/wdz4yhQ1IscpuGMxdw7M6MEYnfh8g1qlwBr9ngTQe50/Vo1MGBfl5Uz2bI5A7GGB&#10;ZqSpzMPQq2VR58gYdf4v7a0hI+fUcIJ3BN82ePHiOWZti9PVCp9++im++vor3N+v1bW3X1xL5iGl&#10;VGhP58Kr64zBwk+ymNIpy/SlKzcRfhUQKJfNFr56dfeFTP3ZgWzXMRvosmozZFbEDBKMn5QgU7Nm&#10;TK518Cz2kcZk1KfbdX6D49sEdj7m5rNsucm016fmstKacqvrKN8jkQe+DqisLqRzVqygpUeSJdaZ&#10;YpbXq3wtz2UdrwUqU2z1fkjL7yervecI8AEUvFojLO5B3pPPAlzj4eYNqA1g7+FDyJUu4T3IEyg0&#10;IO9FCSS1aiiQAGT/SaeIAnmyHMkInibkPKX1ElOXY1dUrmZARqWRoqVLExxGMHYU8dD3uB8G3HUD&#10;bnc7vF6u8CzMsHQNnCmTnsR9ZOJAm+3ZXj0KLvaPfdrIWoVuECYGtPJkUuuCTw1ckIBCgvhzvXNw&#10;TvJ4myag6wb0w6jZDACRz9qd4TU2pkjma9dJc0ACoVdeTosWjoEdgIYjWpKF8xAGKxqUg/OS1xuH&#10;Ee1shqHvQWpFwN5m2W/IZZ8nWyh99mbe4qwJaOcB8+UWV1/fYPOwwdCP8F5KUidOGJMFxkmBq8RJ&#10;Ua5Ds5ij0ZQf55wEydgGJdPa3nLjE2lzLUKMVe8PhoIeB8SIYeiwWW9xfXWNu7s77HY9ht0OLjpQ&#10;cjkwjzV62nzCUhWVNQOlGCEjSxln6S8zdY1Y3AcA7ZqqtFQxM/s7x7rYOujzT7QQBRP6BQVQCZKD&#10;K3PmQNKTRa/NWhw08Zi/57xDGnushxGpG7BeL7DerHD27Awnp6cAgF4tQBkUQYChqwbEVOvLTx+T&#10;uKbK+lIAh1Z3hXa6rbgHkWlswlwM88nzAD4xRiQ0SGjh0ABoCQjqC3bkc3nrbBtQYJ9MGzS3lCkV&#10;avXzXtxLzCGnc3MVwGO+V8u0sEFnixID4AhiAVfBO1xcnGM+b/Hs2SU+++wzfPLJJ/j66g5dP8j8&#10;khWjguyZlLL2L3UyUuWOIe09UiRbDVYLaKgXQ/jZ/ttUrVOtYWdTPU+IEQWWVDfJArAA0mzh2QOX&#10;ZbPbQk/HeOwo3z345PgXvuH4NsDi0WuYDkKivGSWWlvC7Tnzo5Z4KRHkSSu5pvy5kqaCMAUAELqV&#10;GiQJ5sIgVG0a7N5AFqwqWXSsuj7ewXsvDbqch3deAtuDpHFS46VhZ9MAbZDfjQc1QXm4Ao3GIwVC&#10;dACcQ3JeAjDzBCELXwP29VqXjBlbE5mBCr2jICJ7Va+b8fmyHmbtt/vXwa2ZMickTAC8NNHc7bDu&#10;B1zdP+D1yRneP1nhfNailcA2JB7BYAQvoFC6IzuA/CPgwiBgRnMKKDIa3AteMoDBCYjA2CVwinCx&#10;ycjNOa9augOFgNY3aHzAtuvRdT3GYRRicuLfteBDu37O488MUX1Le+A5MhB4RIJHA02LU1RLWWAz&#10;HDVoVFgMgwQpltUsFz3QLAmKGg1oaKBk46R62WwG33hcXwXcX91h6AZxE7iQ3TvGAFmZJzvGyekJ&#10;ZvO56Hjew3OSyP5aszI0ijIUm58JL2IS7YAJKSZ4HxB8AECIY8Q4SlDjw9097m7ucH9zh77vFXQY&#10;8q9aoSuQMGbJUHChQb1Jz4uqiRASmK20NJfr6FGDtmPgIropqOPq2c2qJSm1Cpo0KdxJdRowO012&#10;d1pNVvMmqGg5Yl52BrUQU0T3sMHQd1iv77DbbfH8ZcJqtULjvAR56aqZ28JGaSDDhNRxfa5eH3M9&#10;TLNJyhwUzqLKhhGA0H9Klowkv9WECwIiOcmIQsIIQsMy7hlcZmpMqfATY/QqIE17r12jQvJiwSEn&#10;gC3GCBqlUVnKdSbkGq5ifDpw/VWAZoljYiyXS5ycnOD0dIWzszN89PEn+OzXn2uFTwl2i1EDy0iD&#10;dbOlqgCcMp/QZzHBrnOa5zlNmWy1ZqwfkGqHUzcE5/9NCMhjHUEsdh4A7H++Dyi40E+esafkvH7v&#10;m+js7+TI+JoyIBTkqjTH0D1ohd64yE5oyq4jiRdS8CBZbApyuV7nym0IVZycQwQwMIo1Dqz7RN0W&#10;TvpiWewCeemk7ZoG3AT4dgZqZ3BtQGha+CYAQeIhEARIsPfSW8tZzAOV60MLUgFKQ1UQbzVNBon3&#10;8aTFMhjwLfDBQJKcZ1czl4l9l4BcQ4lR8d5aXlL9ff2SZsj5RHAQebXmiE3f4abvcDUOeDN2eL06&#10;xXurFZahhWcPpBFOU8nhKYO+xy0XRBXRV3TDVUwGIftBpUujmOIhcggxMniM4CbBt+IOIefgnbg1&#10;xFIxQ3AevR+kLTcn1Noc9NrIyhRPhCyjID0GgATsYpIsEwIaAhI5NBCzLjEwcpI+ItTIdZxHNAtK&#10;pekcM9GbibYGkcxRfNaOEFqHs8szzJYzLBcLXH99jfV6IwjPhcn4icS3lzhhebJEM5+BtSiYs0Ja&#10;rP7ZidujrMf+YTEeOfzaaQ+DaHUqdljfPAiouLvHsOvBCWhcABIwRKvSpmBCUwEtot2ETVSmnlhL&#10;TXPMhdaI61TUor3lMR4UFZr+nar36rXOPncqDGai+ZMUeQNLN8pifrXAJ5fdQmaBMaDoicT1MCYM&#10;mxFvfv0VdusOz1+8g4vLC4RZo6dqvAxM7zeVCrmsuN+zvDx2HLNawMZk61vHByjpcSUoDWSwzRtJ&#10;ajhrNc6k5woDFDdlozn0ztahEpJcpeAe0L8KC69dV5kcojPLYwVGyQRu2SSydglZrkC6CEPljnMO&#10;q9USH374HZydn+Ly2Tk+/ugTvHlzg91uK6TsCDGJxcJ7n9tIp2qME5Bga8T5v4lFoj4m1qR97l19&#10;LhjsMbH+CIh47DXU1mMgZp8Ojl6SnwYef8dH7UIXMJEygUojOMpKShGZto+tswpLw0wutC1LKDIn&#10;KuAwPiwl/VW58B7ReyTvJLMiSJp1CF5czsHnGAjXNvBtA9cEybrwXutEBLBaMEhdIJxdKGJtkExG&#10;ytaPTF0siqxjjV9KVMiviu0wJefwKPtv+l7FEybvVG/UL5R/T8SXYWxdp/oCnD9W20/SRpTOIflG&#10;3K6xx+3dNb7arnHdX+D16TmezVrMyFcXKDFz4ZgAzYfysPxAmWZIGQ3l8wyyinkLubJnilHT2UaE&#10;pgE1raSFQpi/ax0a79GEgM4RumFErwGUtVlMALAxr/3xFsNQIik2xACGNIqLxAnBsXNoyIOSMEZy&#10;QuwNRSHMYQDiWMz3QCZ6e3LZ3MVsnTm+GsoTMdzMYdnMMWtanJwscHV1g+vbe2y7QYQvSYtw5zzY&#10;Ac47NLMWLhAGXQ+vmp25Hooyy0ee//DgxBJQRIQ4Jmw3G9xc3eDh9h67+y2GXY9xUCCRRGASMzAk&#10;QeB2XwMIWiSsBhdICRFanAxRU/VjTrsDzB9X/pZnMHCRRzvlu1x+GfoHCr2xvac/dSoqjPkyISUt&#10;3GafkTAJT1IS3Sp0giwoy4OiuKRSF7F+c49+3WF9+4Dz5xc4PTsV1xUnJKo0Lx0Vgd9maSbg9dj7&#10;JWKaAVfmxgrYmUm3ZrxF3rFY/5gRNcIdqklEAAMBc+fhoRVuFbwKlfMUvNfAkE0UikbqnAMHiEvK&#10;S4ZOjBHWeIiLVM+6l+1S50ktlSz+aKJsrZwvWrycvcDZ2SlevXiBjz76GL/66GPc3tyLiw8s/Ywg&#10;FgxwqgKAGWMOqJ7SElAB7+ozAxv7y1ZcJTb/BriQ+d2ktsRkIY8BmBrolHtMBzn9PQE8LOcfAxb7&#10;8uPvGns4IMdnSXlqsQ+WvVEFJ3KZhbyfUwKSxElZaqMUISyZF/DiknDakoKc17TMgGY2g29bBQ0h&#10;p2vaa/aElAMv/1/y3qxXliU7D/tWRGbWtKt27X2mO/REUQRJQRJlWw8U1KYlQTYgWIYF6MkQZcKw&#10;aKAt/4jzt2zADx5e9A9syG412U32vd333jPvqsrMiOWHNURkVu0z3KFNW3lQp3ZV5RDjWt+aq3u6&#10;RtrWT+VAXJruK9mBAkQ7YX0wfC6aF/YLa9pgodQOv22bqVCcK5Bv66ZwIuW1/r0txBpaF4fZ8tha&#10;EyoTkMlvBzMfJb0igBCyOJcyMu4448AZr9IdnqURvz4d8Ml6gyfLFW6XS6xiowKLNPZezYUPij0Y&#10;mA3wDDlRGYBgfWUgk6jOhyzx8g1nxMUSBAgDhVYZISDSCrEdwIcjxpSLTV83c7ZZmFECqiJYiDXV&#10;NsRPYMzCCMUU0QAR6JqIzBJJIh698HLtYQglkRZMojDpUvoVdFZNy+G/K2JkEvVtuwi4brfoFh3a&#10;5QJfPnuBN3dH5DGJx32Qjdctl+iWC5i+nSBZT5sYNU9GcSZ812FRBpwVEARCHhPevH6DL7/4Ei+/&#10;fI42tKAMdLEBMmMckzrPanCSRgRMfSZK7Qm4KSRpSW1dzFaQJ4+TNs19L+5da3rURY0cSFj/HNAW&#10;YjDXZDAnNZWIRzMsORbkc+YMBMnrwaoVYxKv56AFvmKMQGAcX91hOIrGZ3w04Gq/xepqLZFFpHlM&#10;eNR1wuqs+Y45qiTUudnN1pP8BqPAYCcEWg202gNUbVX7gyHJ2SQXhuSeaUhiz1MgMRmCERloWPZK&#10;9IgSmgrnDgr1Px13q6Zs8fkyF8m80UqBOCXHpkeqwbJPZ1SPlSwOn8tFiycfPcLV1Qa73RY//enP&#10;8PmvvsDhcNLoJOmk+edYRFcl55xz2XMMcPFw4xbXHa9vQhUdv7CeLzx8rnGYz3/9fsmh/lL7532Z&#10;+He8B8h923GvwFk/sHpmfRRsXNFOqE8WULQBEI2D5XA2gVUy6ARNCiWah9BEiahoGlDXgBYtQtfJ&#10;ayEmjdg2shabBhQbTTilAEH3TCIqgAKWLyY7byYKYlFVfzPMNQy+12QdGKN2OGtLh6oinZMhk7Ux&#10;wwI+yazAwtrrvnKOZBVY1Hve9qXdjrik+j87pmbLoJe64yekLVmLooZMCFm0/ykEDIExUsYpJ9y9&#10;fIGv3rzBi6stfrTb49FqiU2IaAjwdA/zBc1cDVT9DsDNJYbO6z3onRUvffjCYkkfPgjjaCGIkWKj&#10;dTSAJhBiJ3kxAEI/jursORbCytI2x4IWX1y3kcSZRqoSakphZSA0qrTTirmEYPnYhTA2Fk7EksMh&#10;jXyZCZgaU1dH4uR/M6StIRDSOIKZsFwv8Lh7hG65wlfPXuDw5ojTUbQYOQO7zRrLRadjIYykidFV&#10;bkOqUCxN3Xfqw52BlPjlbHwgoGs7LLol2rYDBkZOLHkqAJDWDgksameZ0KLqNiaRWSV9NuBnBZaS&#10;LAnVdMwdYYvK/N3AAnAYOV1z+k5c3kmLd3mf1bEz2qY2b2wmIAfkYI6tAST2J/nbnyo+Qw1F5HFE&#10;iBI6yQl48/wl0tDj9ZvXePj4AdrVAt1SnJYJ6pzLDM34dc8MTfv8Nge6ep1ZWXSX4qq8M4DMu7up&#10;2DkoEtYAqQEwQhxEMxgti6Nnp/uqtUgesIbAkhM822VGFl2CdklPtBENBXDI4GnUiB8AACAASURB&#10;VCCCBMaEjGQaWtGOab/N7McsETtibpG05Ja0q2mihAe3HXa7PX7+87/AL37xl3j21QtJzU/F78LW&#10;WNM0Yv++OLZVuzHd02emu7fNkWniZvCknFvRx8nTGfXmfRvAsMMl2ov7pLx/G86Yft/3BBZvu57V&#10;oTwHLTvPjBRY/BU09F7yPEikBZtZIgaEpkVsWsS2RdO2rpEQh8lG8kE0Ch5i8OgNJgnHnkRUwOT1&#10;ivnqfwJkgub50TFke1fwUI2HrxcWs0YZ8uLgj+opc88x14C5trvCC9XFhKyhuJgCUrvc71Vdb5nL&#10;LqDKYjrViCuo+581xaCSgYzAohRIkliyoYiWgpQYSAmJJFHXy9yjH484jQPuDgd8erXF96/3uFkt&#10;EbnO0JmtN9KS0hHv/VQtzbXiENXvItFwFiYEKp67OWfwOOKUDghtFGeZrlMJXZhhDITNZo1uHHE8&#10;nXA8EYZhmJRoriCW/28KDcuvwQQNndNsaBrCNrJUXVy3LUJcgKKCFC8kA8T1Cv0wACS1UWqpQwJj&#10;ZrU5YN7xZDsdpl6OaproYsT+Zoflai1ahC+f4XDsMXLGcr1E00kqdahnfAiSMTLFiJSThxPVa8dJ&#10;mQlSlTRIFN3hsm1abHc7jENCYMazXz1DTqLNCYp6Bc1KDgvNGVrABQxcFCdN11hw0uvh2g5p6lzz&#10;cQ+4MLWBe8hZbsZyFHNBsV+KI6eCYt9oxeOcTFYmAlhCpiTcrYROjsxeJE5K02dkjmK2Y4BTFjuC&#10;RCxLTozjAce7O1xdX2H/YI/NdovYlrLpnFGyS9X9LLx6yjxsDfH0XAf8gTy6wuafjcBU697HzMaG&#10;xKXMtRwkoCGxaPJaZfZCnEzCk1wgRuvcFu77vgBqAJMKqoQg6fIDwEFMJAkDMCoYIvMiL8KIRwkE&#10;/0JOUR8MInFIXi4X+OSTj7HbXePRo0f42b/7c3z++a/w/MVLjOOAEIKACtuXth4xk4+qPez+Yvcw&#10;ZXdmvXgOFyI/YTwuAZXn1tfqXpv72hj9KAJKYWLvOt7B57/z49L4JGMcCu4TRFvGXYdms0azWSEs&#10;FpL/aNGB2k6iMRpxkkQUzUPQ8gUOYs0sYmDXtj5ZhlrVGZls7OMNANMCYXYiqZDpIZyQ8c9UoMHc&#10;N0wrG2GGFKtFRs7vz+Eln387AxfBQmm5zG9xPubJe6H9Rvy5Prl6Mpx2BKgAXN3fBHXRJjESZS0N&#10;YFpw2b9Re5bBQOjQhwHPxwH86iVoTNitlrhaL0VhALDXhyA2x7cpgTeM7i5s/mNh7kbohF1IrJsM&#10;s25E7QwnRsaAnEexoWZG07VoQiktzhFiQwsLxBBwCEA/DBqhwL5ByYgj15uzEBcCZHHroCYAERlD&#10;yqIOQ8AqNuhClIqppOWfQ0DbtAhtkGiPfgQncT4zJ0AGJGEWSahrtvWVE2KQyZXQP8AyWrRNQHPV&#10;YLHeISwIXzx/DhoT4ipIXDfEnyNQkHh4AtomgrkF0ohBYzhJVcESeikM2WyXsnHMs18Y+ogBoQE2&#10;1xu0XcSYRjz/4jlO/RGLINVPeUjIiRWgaT2UzBi1TkjQ50ghOn1OGoWQKtiw782cxLly9JtsxPpP&#10;9V9R3ZxigenGMMKdeUL0AWOkNchTbYVw+wJIQUBWhy2S5xIFsDkQc4CFzo3Z7MT6TGPfTMgp4fX4&#10;Ev2hR3/ocfNwxP52j3bZ6qZTp2DVQlmomxOBagCcMdv+019LMiDWdSRaJfnNnDRpMqbZiCSMqNap&#10;shjwBDdiux4z46h7YiRCE4A2tOiQ0Y25mEhQrSua+mSYLkO0P+V7YwqkY0+jgFKoc58Ho6kGwJJ3&#10;scJaSWZHJjyCopg/dtdXWK0XYiq5vsK/+9mf4asvv8KQsmqtZF+IWVbu5RIoFUe8MvK23s1HSMeK&#10;g7OiQvvmbML+EIdmD5FE7cBXnlQuogIyAbfPA4WOzhnD7NfJ7VwaVuAmP5RImflh314GJedAFzjf&#10;j/699qeCyo502eiYnjeCELoFdp98jM3jB+CulaiQGCFpqqUMeAqAxEoUYGL31sdNpXYYyCs9INge&#10;r6FlGb/Cv7ga7ylwDpN+VSNExcxyeRCrXX72e4HidnOeT6t1sv4IVBp760PwXk2ewLNVQmQ4D8aR&#10;iQoG8VwX/jzlpBzRZKDJDCBJ9VN/omh8mNW5PTB4HNC0EU3buFkm/uRf/YunRpSC1XS4BzIX7TSd&#10;9WoCRsiIKZ39bojOwkvtRcq4g3aCSDJHSmVP1X5wbcctDTK7a73AQrVLjdl41QhSBpqLegnQ9MxR&#10;VXq6SUz7EEMUh7qclQlR6Z8RQm1LYPO5LW0QIpoFlsaA2EVQG9BuVtjud1jtNmLnV+ILKsXgJOMh&#10;tJ6LEmOqbPMS6F0YrQMwGWwJo5TkUW3XyALghHEYMY4DeBw1AYr4I4wpSZIyJ47S75xES6EJTYCc&#10;DFGhhJ1mR7rMBi6yt8lUyrAt6lLE1BmSz146l29RIfu1lRnGgQ0XByj7PF2cFQFS8FHvc+ba2x1I&#10;Y8bpeER/GpCTmC2aENEuFtJffYatZU2TWYARiSwFiONakXWgjGq6lgPUwQqEsrpq+FCTiFBtYZrs&#10;Z1+3zoDFgWtEwBgCsubGiDWjBMHTiBv/YFQtmLBQJ8CkZkaTPgXMUbmJjitpn11W5NIv1OCRgNgE&#10;LFdL7PfXuL6+RoiEw+mIU98LYOSyL4JqpEy7xYwCGomqEZuBXyWeNk52MVFlhgXgxiefr5rNVkSk&#10;fs2WXQGWOpIM1YZMX8VUMf2e/Tu45vgedDK5+oN+O+Ng51ext7Ew9iieE0IqYkR7vcPN9z7B6tFD&#10;YLkEdx24aZGj5INIMSDHoCGkFb2+j9FXTZmMChmPYcxH8q0dd751udvvPGb3eedJ1T72teCMp9LY&#10;UO0OXdbDpX9na6xas6ahuTB903aruahhNZdSxhgl5bf8HhTIE4CMjjOuY8D39ns83m6waiRBWPzJ&#10;v/rjp9JYOOO9b1TfaYurG3zhNqT/lckjsPo3sEo1RASKUQmBEIgYoxR3IWl0cfgzglcWtN2XACeI&#10;09VifWWklDEm0aAwAFbHv9CINwuzgIW2aTWbp6qIrJ3QwmPMEoZrWQxR1O+shNnHQsc3NlKIplsu&#10;sVyu0DatAxnovYQGkt5X81ZwSb/s06FcMEABkrYvoLTTHR4BrJYrLBYduraTiqenE8ZxFNtoFht5&#10;yqOCCTWDjBL1I2mLkwMGN5Fw8c1gtnDWAirqNczu11EBL/+LnRGAWbN82nfGnEu/Sd8dpJw5ohZQ&#10;wyBvs6muyzmowA9NgZCqpexZtgDHYcDpdMLxcMDpdAIyI6rXegiG7guADfXih5GU2frUh0w0HTPq&#10;aPNo62NGT2BpeO1x8xoO5XEG0ORz1vGOOriJTAtUHPOEQPHUHFP9YSYdJlmzQWsiWD0YIZQFeJlm&#10;gNy/Q1by244YA5arFa6vr7G/ucHVeoMQAsahxzD08PXExT/FzSw5gTAHA+zXUNUKqn6bEC7U4E+v&#10;YMwmrNqfNsgzIEy2vnxhzSn/9KALQh9NgOP9oOIbHfeAC3egrgCg/W6aZApB/Hu7FlePHmL35DHi&#10;Zo3UBHCIDiR87VQLStZFKOsdRQR4a3Pfl0+9Z3e/66Nebf7dvA8XGvU+/Zycc482a36wSqVNZqkj&#10;RYwxsrbSEIas78gZS2Z8tFnjhw9ucbNain8GA/Enf/ovn5LZrMiktvdo6Ft7NHm7+LuE/sGZkyXD&#10;yZnF4xeqkSAZlEBiV21bcROxLH0umSriAsHtZ1wRrLIhAHgMtTBCqzCZVF5tYkQAibaCBH2TorUQ&#10;o8s5ibOqwCWctIQHkiNA7YDTJltIRuycOWiSmaj3pxgcBAWtmGcZKa12B8FAREkOZfNEF5csdFwj&#10;2rbFen2FxUISd/XjgNPYox/HiolmjONQNBas1VVnVU8zyuec2R07XaswW1DF/4KnRNe0ERZ1YqDB&#10;fuMKpDGm113QaJzbg6ej4dJF/ZwJ+SrAgybtyxoqrA6J44j+eMLpcMLxeELbNCAKUg45RFd923wV&#10;XjUlyPXrbUS0Jup1txxATjD1OcOqtW5Fk1GZEThJQjSSED1hEir1ZDE5usO2jqM71UKJ02TTYQI2&#10;2Psoe6P01dbs2+mMVx0OhOVyiZvbWzy4fYDFosM4juhPElGSWTIGThzWa5AmN/MP8rnSWuj4nYHA&#10;iQx5DhIt4ZNfX69V7wPXl8w0J/cflTBb2lE/zj/T+9Prdz70Uhvo8o/6lbchRPTMwKLD/uMnWD+8&#10;QV6IScTWQzS/ClQ0wynY5abU0WHz17fc3d/Y8aF9uHTeRcffDxwTc+h2T5bAaq6yb+U5AYwmZ2wB&#10;fH93je/d3GDTRMQsibjiT/67P3462XB0/8TNG35vo98CLurFYc01IpiTZnxMyZl7jEHlGNLiZ1Zl&#10;tVSmc5U8JrTjXMWlH1zzoMlRMiRUb0RC0vzo4mjWqCZCQE4gLXFrTJ6zOoYVICC2aXVc83FwbO7P&#10;lz/VeRMETkn9OhhBQ688QylQNqBJ2QBM+x5MBcvWt2qhzBebAi9kRmxarNZrLNcrxEWDkRNOp15S&#10;ouvJgQC27Ii6oDiZ2j+79MmsIV3MpQ7GDDRMGqEg0Bmg90tNKuDJ78avClE16bQ44pZ3NTLo3/50&#10;vdadqhQNnm9Hvb9FI7lWQ9dqZq09o0YKBvKYMAwDhlOPvpesrE1s0MUGMTZuTslc0hS7ltCY81zy&#10;1N/OCKzuRXNKhPXbCJPZGrTxzler34tp02GGDoc5dInmwuLESRtJxmwr5sJVH7w/BsSMbaoGLsRi&#10;0rMzypLQZ1RA+dIh0SaFCHddi+3VBpurDTbrNYhE83Y6ncBa/l1antX0WMCoza/Mo6+cs/VgoYCX&#10;6Mn83DqKxIHejHG8vYfvd7yv5Pp1GO60vZXi3eamXnt2TX2tjmMmYAwB8WqD/SdP0F1vMUZC1kRs&#10;gexl95V5AMrYlR1KM2BzfryPZuOdff+G13/bxzujyr71o4ALQKow51DgnuJAhJyx5Izb2OAHu2s8&#10;3mywZEaTgZYD4k/+9I+fVvfEdLm8Z1PuUeHcd5dgKLU6XTQEcCnSVNcBkj3RwIRFgbgfhBJlU8fX&#10;tQbmdi8qD5u8rExtMrYzCMjIgCaiklhrUxGLJkHtyUroa1+QugiNPM4oTKVOtQkKVlxKQFVWTQrI&#10;cKKcGx1NkuS/B1UOripfmR2M1MHpnkVJMOmRgEDolgustxu0iwUQCONpwNgP6IdeNUwkpdRzEudV&#10;Lx5VnDVN/V8YcXn2/eF8U02Doy4XJuXeRupLLHllytDzhVcaw5igKweek3bVC6QCGLaWuTaDGIAx&#10;UEgKCK1CKMQcReqOfXc4YDj1SIMkFAtap0AigCriDLvpZHKm7cf9+6jk7amZFrnmqDzD3sm6e+lu&#10;djNwDEi6H2RdG4CxeH4q/bAGToacdcxUu2JARidSxqE4fLpvUcXM3gUuBPyLsCF4SaodX+922N9c&#10;ixYjDRj6Hub4bA7bBlZhHiyKdIyB2rMvRonULbtM9gqAv+e41LuJEPc1GcbbgMSHSMXzc7y9vt4q&#10;YDFrd+0LwEQSGtq1WD+4xfWTx4iblSQ0VFpVoG2hhcVPgHzvnffvsq7Hgew7e/mW/n+Da7/t410C&#10;ff352wpHrncBUwmBt3El3UshJWxA+Gi1xo+u93iwWGJJUjQRYxazCFBJHTgnbu/VoKqTZKrUezQg&#10;AGAaB9vUgco1gUSST6NEaUjVUtEcOJ0KkrwnxKDJqDRPAVc7wAdriq5dNY1CByxHPLNoUBIzRpZi&#10;bAhBqtqR2KVDJGUYUfvKcB+EGfMwci5/C8E3YpoVEAnOyBqALFUnc0qigtZ+RypZO0OMs0Vl3Qnu&#10;wDmR1p0fGFMQ7QhRwKipuptFh+VmjdVqhUXTgTnjeDyKwyfE1CEJtBSCVRk8nQFzMYkUkFHGoDAd&#10;FCZvEqQJ21zAmM8Zm21efS7YNAvsbZNx5ApMcHlmNQcm2bC2z7UYPkyS2GcieU9wDHuuBPFFZpe+&#10;tGSFJAYaR5xOPU6HI9KQHBQ2MRaJ3YigbQlfMxe4FmH+rcy4M8Zq/OyWRvSr86XbPNkfdV4AJolh&#10;zwQwk0anaN/9xuZ0p2ttttfA5E7NIPWgIB9ATx5nAEGS6QXTicw6fn4wxBQJZvEVSgkxBLRNg6YJ&#10;WK9XuL65xm57hRAD+v6EYewrqdaYoI2rfjs3Z5RdVc63ca7GHTbuij7Pzp28pj4ml4769/Pr9R58&#10;3j4TRL7p8S6NdFm3l2fL/E8yEcZAiFdr7D56jPWDW/CyQ46xrBkGLFW8OeAG5QHOE+4bqlkzawHh&#10;/y/gYn5cAojftvaCIPtX9jcmgjL0uwBGlzOuQ4Mf7q7xo+s9tjEipBHDmxNeffUS8b//0z95OrmS&#10;7Pb3H24vrIlK3UEqzPVt19vjRM1r9KeSXJUZWdEsQ8XBoiWChL1FZ/Jyx5xmGYXqZwFubnAbghHK&#10;SpXCJPkwBs1CKc9TkMGC3AjkGowYJB10ThmsIVgWAVOnVwaEIZGlQNfhMiYnzCIDKYOT5KUIBqi0&#10;70xQ85CWtWeWmgtqpoBhJ+1nHXXiWh3tk5X/zpAolq5tsV6vsV6vQBRwOL7B8SSVYwMB4zjCfLgl&#10;KqOaM+9nJUnPpA07l2fvkzVUJVuCMf9Kw2EAs2g7MPl9CuBrIl3CC8u5yhCq9THNEMreBPvMDPX5&#10;UQZDYZr1TosXpWFEr4X5ck66TqTiohNm2yfVBr5v982/dSZnv+ke8ggJa9IEUZU5KcS4RFCADFAQ&#10;3GMd+h2LXxJp+ye82IARS9KdaPeroM18XQRNnEWh3MiddgtEujAQZf2h6i+r7xYRoW0bbLdb7Pd7&#10;LJYLjOOA0+koe4/nBLnIaqT3t/1oYzw5e07MK22Yt5pUfVytrbkmkWj6mo4Vzq6/dJ+6fXU7v4kM&#10;+05zt6/d8/N0FkEkkR89EZrdFtcfPUa7vUKykuXeZouCKg03zbADpottnLZp3v7/r4KLd2mXvhsz&#10;SHV/QM1SSj8uPk7A7RqEB7HFD7fX+HR9hdj3OL5+ia8+/wK//uWvEf/1n/7JU1CJKQdUWXhPJ00l&#10;+dYFyHi/BDDG9GppCwCpNG9qMTN3sDLQYPa6QBNNRtNENE0Dy/NgORYCPL2Bggu4hCvtKOTFmDIH&#10;yfmRWHwJkpZLB0FtuAIWApGn6nYmDsmfAM2AGStCw6autnMNhVRef4GBqBI6p1FCRbUSZBMkPDe4&#10;9BdAjYAeARlJTTi6CEIh2qj6a0yHZMKhbBMEoG07LNdLbLZXWK6XYDCOpyOGYdB6LAHjKJlToxUH&#10;Y9MalHksTp/Z+z5PSGP5E5yDV0R2ruabgInZOqp/J+OqVBg1WQ+5EOuy5ItqInOJYgDMybGAv/KX&#10;taOAJAc2zLI+EcApYxwG9H2P4dTjcDxI9FHTCDhEAcIObjShm62nWqv3tsPV+tX6svEp/GCqyXBC&#10;bOXkCW4G8c8ws6GlamY1JfIkv4RImaSgWhLwEJWb2J6m+tnBCJmC91CcKd2cZYQOULNnaVzQveDr&#10;HfBIqyY2WK2W2O/3uNnfoOs6DOOAcRwxJg2D1qeZpGyAWTSGZU4MABQwai2rIrds8XPpZR36R9Xn&#10;CViwdfQexyVzxPyofT7m116Seuff38ecJ+3VTl+8H2RNjAEY24j1owe4evwAcbNSrZjQSB35yVgU&#10;+Y6KdtrCmeszyMaiXlE4A9AX+/bOffTNj4kA/S0Cgt8IuAA0JJpdq1lntg1ECDljk4Ef7m7wg+0O&#10;zd0RL379a/zqs8/wxedf4O7VHeK//m//66eCFqlKelImYJ6w6IMa+p7nn0eEc7Vw7FDCr1EVIGi4&#10;n5oNYrHdxqaFJ+axsMjZAKJSwhIqhGzAQqWpDCDnJP4YrNESEGdTBxTa1yZKhIARW1iaZt8xRfti&#10;TDShIqLWnwxEtgJw4uhZh282sSn30kgVYVQQ4mhEkOFaHmOqoUpWZvdT2u5EOzZSbrdbLLC93mG7&#10;3SK2DU6nE94cDq7tIB0Pzllj2glpHPx50wiM6THVftWJcu8nsnNgcu/vnsaSJrc0c4kvS7NDTHGC&#10;R/2gGkfdGM4s/VlEE2bsz3CmIgwrpYRhHHE8HNEfTwriWl1D0dd5sKqjBlh0vb2XrRy2hqvPhr4B&#10;B5qTa2b3lXVS0qoDmj6YBGxnIo2qKqm+pCiVahWhtRlcrVHAdJmyOUtV04rlxSDZVwbi3CkU7Pd0&#10;EDHvPxXgRwpYFm2H3W6Hm5s9drudRJT0PcZhdPAGE0KaUA2k0A8rtKffnI13ASBFAzLtZQFWlw6y&#10;gbjw831M8m0HXxA37wMVF591uSnl/sbYaXpmERZlLodAwGqJ3UePsXxwAywWUuvHCpnZtQUvlO2o&#10;zExAX6jGIMzaNtMazYAREU1ozfvuo2/z+H8DXFw670NoCAHFJMKVvxvJPmlyxjUafLq6wmZIePXL&#10;z/DV55/j1YsX6A8ncALi//Df/Mun0URZLhRlDijeqbF4jw7W3uTOXzH/XLQRZe06N4CE/lmeBVLn&#10;TlKCLG2MsVX/hAJWjDmbmtv6EireFpTL5oDigyGPdalJyktLKGgMlkzECFkUMw1F9w+BXsdKQEs2&#10;Qr0nKq9/UhVhliRGkhTI+JokpTIbM0gSCoXo8ioCEZomIGutEx/HavNOJ8S+Jx8nAVdBwQ7QtA2u&#10;tlfYXK0RY8CYEw53R3Hw1GcawAgkoKtkPLwMFApjnmoq7LdQtadeMkX6xdn38hv7ppAxrquz5gmI&#10;sENXt4yfL8KSi6EMVWF1oNI7Vq7A1b8JgazYDScJ7b073ElKe1Phx1ZSjns+jQr2Tta/Esv5/pn0&#10;BajVhlT/pwTbibARYi4s09aL2e8trLqAC7Glm+xeNAvkIdsupMCEedOyGeYjW9QOHOw7UhNjoOAO&#10;oK5xIkw0HaWP5JSwtCeUomZWcXW5wvX1DpvNFRaLBVIe0fcnLXoGr50z9dMxx9FQ0bOy56b5LS4B&#10;CouWsTm7vIjt+3dTV667fuEoDF/WdGG4kz1z3oSLv198ghJnZ/QVXzCAlUEYAiHsNth+/AiL6x1y&#10;0/hOC0QTAH75idU9qcyDPM+Ggyfg4VIp8w/VIrw/h3v38W1rGi5pri69vllbyhr1sFS2VBUiVHYZ&#10;2HPAtk/IX73Emy++wun1a+Q0ApnAiRD/5J//s6dtbGXjqmAgqlOeNPZDPFEvXVNLUP7d/Dp/58lA&#10;uI5B7+cREiyggbJmMDSCQuZ5LH4J5mPhavNZhktSwmvMxQq9lHbZ87NrL3JOSEnlN6KJFGXlhYPo&#10;VoVh689W14qJHFRMpMsMVS1XiYqM4elzh2EQJkyW4wIgYjRBzDPRvK71gZ57gqw6KIp5pp4z/S4x&#10;az4PrTZJjHbRYr3Z4PrmBk3boh8GHA930o4Qi1kAU/PHJMeAzacTA778/YWlNll/XObMONjkCVTm&#10;urqBzXT5nurfjJlW2orq/gVUFEgBqhhH1U7zv7FMoYUVsTuADkOPu9d36E8nEDQiCloJN1ua9+Jf&#10;NBkLX7fTo8zl9CWRRFMC7kSqDCmAokW03yyqiP1VfTaAoYxdQIdl2TWHMBKHaCrMVhLCVW3RRp+p&#10;rwMq8x9m40DearKUutU9amGo5JQICCHi6mqD2wc32F5tpMBgThjz6CCkgkzyfLOp2ig57bXnzWei&#10;AsWOQOrhv7DAqTq/1o44nZruJ5vb83sxaCLpnwOLmgHNf5tH7piGYsq4SId7apKw34LWtembgO5m&#10;h6snjxCv1sgUYNkdw0y74rlR5u2271D2Q20S9iEFVxriWR9na+tdvOzbgAMfKoh/yH2/82t1zRrl&#10;kvIgJclZZMYqE7YDY/HmgPzyDjgNoCpXVcqM+J//+MdPAwOLrkPXdZIdE3xp+b8TLRW1WL31CzE0&#10;4uBgv6zTqlNCgGCLuKKkBFKbv5LqzJIbIlvNguKgGEg0C02UXBlF4mHVGKAka/INbBKotN76EVAT&#10;GHlPmb2UO0Ol/WAbURmZhaxG2VTyXFFzM8ilOP+n0neoxoMs8yeVNgGMcegxDgOQs6SdbiIaiL9G&#10;Exs0JIvCUqsLjQiaSpydiRqRBISxiLbGyqnKIiMCKAY0bYf11Ro3tw8Qmoi+lwyVwyBaDApSHbTw&#10;7CmRLBEY+YzITsBInX/dX7jwXU3cy2eJGimPqCz3xuLPF3f1BK7OhYOUaV4N12L436btEJ+AUktD&#10;v2MGNB1yCFG0GP2A/njCcOox9AMiBSyXS60MzC6VGWH1cSqo+PL+mdEQGwdnLOVb/Ss4YCs4V9cw&#10;1ffRf2b203cpkKZ1SrR9OfBkT0DHBlSFnsLWXt0JFB6ujMQctiejXnd6kipZBAzzfwnum6VrRZu+&#10;Wi2x2++w3++x2ayROeOg0VGmhjd0aXvFHU2pPK40pezjc+6kZ9ianDDq+4/3kUAvn3OfG+Tb73/v&#10;86r3GmzYYNZzSGrySETIiw6rR7fYPH4ALDqpYxEiovlZVGTeFh7Nhm8CMrQRlnxL6H1ZSyZw2nnv&#10;Lw5f7u9fxeM3Ai4AWPmOUEV+5QpcbDKwT4TloQcOJ6kzlcXPMSWJtIz/5A//3tOh71Wtrel62wAr&#10;X/11YmeNOfqVNYGqN8b5z/oFw3W/9j+hQrTkixPQGiGZVfsAT5rjTlkhgCLBSvRmhqftds1DJYnB&#10;ABKjEjqU2FAZfJBI+SlLLQ4QqVlEdggHeaY5JFn4VarCEWSLsmtXZMNp+pILaZttg3vSrZwllwID&#10;DRFaVSk3ZJEJRT3MDIxe7KzSFMAIBKvkKaYVsG5kBZxZd39sItabNXa7a7Rti9PphMPpKGG6BoJw&#10;vnbK53zx+9Iemp0/Wx7vIrom3dmskf1m4KYScfTNJEIBNzb/xXGPy9QX5qvrD6jfLWKH3UmwqECU&#10;4iUWvwLV+AynHsfDAWkYwcxoWkneFpuoGVuhicV07i+Bh7Oj8pWyz7Yz6emV8gAAIABJREFUK4lO&#10;fq8Fg1plM6X0Uzt5QTJWfXUkSUaXKQshCgK6oQmo7HZmvjxnsFzMJrD5VwZS1yhB7QMhLTNlE0i0&#10;dsUlhmBJ9rzrOg1EAav1Cvv9HuvNGswZJzWT5JTcbGWZen3MDOzztA+FYqFiwGV/eV9n9PBD1Njv&#10;xyAso/D7AYz3ATrhHE2d993oKAOIEfFqjc1HD7G83SN3DTJU8wAqFTkrYDEdwArM2LtNHhUNi2mr&#10;ay2KQULf5R/IkP/9BhesfKeYih1cBCnH3jKwScBVn7G464FjL9W0U0ZOrGUkMuI//wf/4Gnfn3D3&#10;5g1OxxPGPAKNZIhsGkm1PXeke1sj6/VRT3LZWHR2/mRh+cY3GFur9UgTFdXPIlAW4gtTQwdjcDDu&#10;LQQhRo+kIE1DXLQq9lw4WjOzQ2lsFTOgjcoQlDakrMlGuOB5NYuQE8ci9Xk6ZX0FUAmRI/Yqly71&#10;wYxDItGGIAACWaIRUt8jMKMJUdoeAtoQPS+GMwEFF0X6ExV3cMlBibOX2VUQptw1xAgmiSjZXe+x&#10;v9ljocW6+l6iIhqqC2eVdePvxOffTU6+vLbeH+jaLGFC/OqrJ2xXtRPmj1P0JuZRITk0TK9VaxTs&#10;cXZuHT5rvgf2DKoWTwylCBoykNKI/jTg7u41+rFHQkbTthr9ZMmfSnr4yVFo7tsHsEADFC2EfWuf&#10;tUGTPAuTM2DREAQBOhkQSZWCVLaEOHyapGN7kVTyDyjaH8eD9QRVRd5snYqZhDxSzCRk37h1DxX0&#10;lSgDvW0gVbYwapNc17XYXe9wvb/GbrvTIn3syeJE8OCJNc1oi4/nnK5xPR/V91S19z6QMWP271Tj&#10;n2kg6jD3twOH9wEXlzQctQar3EM7xYTQtmh2G6we3aLZXSE3EVD6HfUFGB2lWpaE3mUCWCZ90UXh&#10;AKNKI25CHF8Y2/c1Vfz7DS7gfDHoPFmxxEwCLjpmLPuE+OIO8eUBpEIRawqFlDPGnBD/yx//0VPO&#10;GXnMGPoeh8MBwzgihohOi2mZFGQbdRJORdNGwYlVJdiVHXi+UOu/K+GuXrC1JGtEztSetbQqdD8j&#10;DWORXjWEiYkk+VXToGmiZLkkqA+FMQt2Jl+H4xbJ1xhw1JBFUp+KoGiNMabRHSvI1Ln6Mkm2aVqj&#10;nyXu3kdDkoEx2b219/puYYmsfhSWR4NzAg8jxn4QAowquY7lxIiC8jMLUfdsAkYYqDCBQKXdFoIr&#10;08kgig7iuk488fc3N2i7FmNOOB4OTogdxLAx2DruvzBlXwM18dVojvuIawlz9SeBSKVUyg7GqGr7&#10;ZM3oOjKwwL6uCxOeABV/VY7B9bl6vbQpg5O1r9YKlFUvErJU2Q2a+XQcR7w53OF4OiGPCYSA2DRo&#10;YqO5NaoxnR2262rLyaUzJpody3ExueF8vHXt6Qavx8/CUsUBOoBDyYdRl8uW59Vq9LNWwUZzMmZG&#10;M8jWckCMjewtZaI2F9AnhInmAgBEAxFi6RuDS60Thmjj1mtcX1/j9vYh1us1chrRDz1SGmXPQLOL&#10;1q01oF6P4Rzs3DcTl+ZwNnHztX8fezg3ldiL773qfTUb888FXNH8ZKfxsVsgrtdYXO8QV0tw1Hgy&#10;LlWjWQia4RFvaVQBZ2LccWGzei4BViDSzOCR1Mk9FBpTEOp9vayE57K0/Xk1bZ6/SPnF5Ft6v7H9&#10;OsdUq/f2c973+9lZzscDo8wVSOqLEKNLGc2bI/JXrxDeHBFylnQN4yhmEUh+qPhPf/xHTwMCKAEN&#10;AsZDj/E04HQ4IlJE07aIWh+BVSr3vBGWtCYQQhMlFj5lzxroUMCQbiF/mMyh/aL5EOpzoVJDPdkV&#10;Z9Kv9G9dSJytmmfWxSpaA/OAt6yAEk4qoXOcJALFiKA5fWYUHwV3ImJbueY4pWYHiGYgZcbISYir&#10;1kGJMbh61p0uY0QgqGbAIkome0eZFZdPtskqJh0hjItSQk4DUhrAeUAgSNbCULz4m9BIZlGWHAys&#10;haiMaLhjZtDFWGVTBATUSDirnkcZTddhdbXE+voK17fXeHN3hzGPOA4nZJI6EJJpNSOSVMwT8Aav&#10;J2OEPjNA2TattakeDXI7vvhuqP8GMYhU00L2vWhJ7F+29VXnZZj8q76j7LCnhFUqFbTAE2NgGqVg&#10;q7tI93aYg6twfWZxBM7qe0IArGggMZAHRjomnO56jMcRAQGLdokmNpM9ZXMAgmvrmLMC0ACvmTHr&#10;I6q++hkT8ArVutAUQBFgyePMF8cTTqkQYlo9iyjJLC8DRWx7mmK1+01jJGuRVbPgrXPmIDkSBMRE&#10;IJJEaAVVtWcGOLt/FKuJKivaKs/Xkkw6r7L/pO9t02Cz2YgWY7cDRcLhdEDf90I7rDYJiqQuNCAC&#10;XEBNxWV03KaScw2i71+H89/g115idS686WLU3aK/VaDe7zNnUKXxl6T84kRZaNHkn2kRYgMO0R18&#10;AQ3dD1G0ohASKmaz8mxbd2Uupf+2FjOz3DMAHEWStr0fSKr5RiI0QczDMZALcDUNcX1i0jBjYpBw&#10;UnDIyOrM6IveRp8DLs2VmXgs54nPw9cAGO+yDJS5uDxH38qh9wxQDRMJsMgRoMxoTyPiqwPw4g7x&#10;NCAwI6WEPg0YOWHkjDFnxP/ij/7h08AEykBLDVpE8JjdyUw2leQ8CCF42KHnWdAJTi45ks23wwQ/&#10;yQmjEW14Jwga0YDCrO8bVAMb9bgW7GHEnX3xFAkdLkEGCNJtYxCAQSTgJmcAVg8OXhuhSAOAFTg6&#10;l8R0IkiKmqWqTojnsHBNRBm/oDGzGWKKqCM6pirHMi5Fq6O3hxQVI2akPCCnAaxJvMznIkAAVSkW&#10;VFrPznipaExIJFNx+GOYjdkWIJHY50iJROwCVus1dvtrLFZLnIYed4c75DGhbRtEIgx9jxiCRk2Q&#10;F4EzppNzrsZIfuGZaEAKLGxDl6NypqSKkZnJA+ZEW68vsqcXckHzUFqTkOtrqnVnK0BNH2wMuZIy&#10;JqZFZBk3qkCC3Z0JkSNCJqQhYzgN6E8DkFk1UJJ8y1T2lv7eqoCKsilO1+SMrZS/go9zOZvKPqlY&#10;miw/Zdq2wef7z84jyRxru9EyfppxyUNZbW1XwMLZkcbTsyekElCRoeYX0rYqE4kodvwMBgfn3DDN&#10;h/ewWsNCchQAoOzHtm1xtd3g+nonZr+cMaQRKY0+gmxICiRkA1Wtk2rNvkuSvAwuMPtbP/OsD/X9&#10;7OXddRWgfj+NqjoHD4VGTibXztU5dZqktNo+eyRHjJrFNSCnhLEfVAun4KOJgDp/i5OngA+p18MI&#10;KvAIvQlAZcJmj8ArfTTaJ+CCXYB0Z15rs2XU1bEMRkN94FQY8XGZzsJkL8zGvPxdANrbHMffdUxA&#10;6Ft+f9s5X//htta0rhRJIUOQOHM2xx70/A70+og4JhBBTCEsWoukZtz4n/29v/80jQltbJBTAo/Z&#10;JfihH3A8HND3J3BKiJpVkLT2AGIAWfImlsRPrTKOCTOoGn1GkMgsp2WjfR3ENxkbs+ex1gdJoyQj&#10;Mg2GOwEJJ4ghoGs01wBCKVrm0qZNoKq5K2JUt1wIL4GDLKsEKUaWsmhFYohoGs0hocwmxog2tl4v&#10;hL3tloaaJJQveAHc6vkX14XcJ2eMY0ZKWRRCZKG5opGJmngraKIPAYyAJEkpCF3aapvSskWyEzon&#10;Yr4ZAq6uttjtr7HZbMBEOJ1O6IcBANBEIRZeNr7ayMRA5oQQGHCtRHnNsyOGilA42BNdKwiindFg&#10;XbgzIlcAjwGwaLkIWcAZmcTETogK0GDdG6HqMsGkQsurwX7N9NoJyXcPUY1P8tMJoZKQxjzidDri&#10;eDhhGHqknHTeBBQLJlbJ3yMl/FZTrn/fy49KQDhjPDRVGaOoZmsSSoCHrlmfmUTiTGTmEtEumULI&#10;AY15ppPCaNMekjiTaUP0GdULcBMezAF2Mtbzzpa/DcQAcGfWEKKCfGBzdYXb21vs9zu0TYthkNwY&#10;nNmlU9tzoh2U/VSJVgUQXPwbrqGr6Z6ZiC/Ng0/B15FcK7o1ocUXJeEZg5uDjtlnybAq+w8hSjI+&#10;IozjiOHUI49iBmysLhOMFhd6LXNfJH8mSWiYo2gsg5pLIuvL0hBoczkAYwCSOtPHENGGIJmNCSAN&#10;52dA60Vpe5XGRQ6IHBCy+t3B1jEK+AhZs1eW700QtZww3/R417x+V2YXvbmKlLJSEkSjHojQZga9&#10;OiA9e4lw6BE151Tm7MU7c06SWPEf/eEfPl2tVs6QmET1RwBSL0y573sc744gENpFh27ROcBIGgYa&#10;Q0Rk8nz/BBTiUe+k6qhLis87900Ok1gJBM9LYc5hSoRJF6pQYjWRNA1i2whTJ2F0dXY+uW21mS4g&#10;WWGLGV7YiSWSxTzPRY0tTKCNLWJoXAqIoUHTNJJqnFBJu+fEYP7cSwdBQoPyKDk5zOHJM4uas6n+&#10;7RK055syFhcL4XGv+0rO0rmNOq4IEU3bYLsVX4yu63A8HnE4Hibhv6ahyUnQr6QSz6DKrFEnwjJH&#10;Q1LVPwgOhKAMTZharaImmNhTyyBlthigLKCC6iiX0ldnquqfcWYXt8fY2Oh3Z8t4NoZ2nd2FFfyw&#10;+fMQXCvR9z1OpyPu7g7glNDEiEXXebuiZrbMmib+6x1lbkuTawZjmpZLV8kRYHZaONHN6qDs4EI/&#10;mzOfS5YFIsr/zlzqPa2Sbc7yMsxGBI4kAg+VWi+TsOuqDxOmCNMJSk+ymnZtfbZtg93uGvv9Ndbr&#10;NVIaMQwDTv1JNG1a1FAAR1kDRh0mGorq7/JetCz16rQ/zgBGOdV/f5uEe/Y7hcn19TPK+4X7VuN2&#10;NoZKQ2B1Q4IInh6plhnZQMaQRFMexa8vkNSDymAPLTVmbcDSNFWiqq/ABVz/poKdlHNnr02i5RkC&#10;odWEiyAREhJb3SqhcZEbBAUWkYPf1zS0Bizc5ArTdEyBxTya60OOd2ks5ud9N4dSSmWYRqcaALEf&#10;kZ6/QvrqFWI/Iqq2P7E5/2efy9iO49N+6LFYLLFYLEp4JoQ4ByKJSDie5NUPol4nEmTKYkkQpHkW&#10;5KcEtvIOBzwq4hJzlrO+6eAFR/2+BzRcNVkIJ1vSqUaZmqDmGEX1HGLwtjsqoVm7DDhVPbBNYQTP&#10;fspgjDlj0GyARIQQJVU4s9iFDVxYETHTHDjTJ4h0NsMc9UuazCqrC1VOaZQKs65BITAbQ5d5pOhb&#10;ySMHkTU01X1NZAzNlGHe2iLR6xxrdErKDETCerPG9c0em/UGY0o4nI4YdQysQFHOWdT+qlkqYRXG&#10;dKr1o1oF06vEAnHc98FsE6J9KHKsEPeiSXAC6jOoDAlTMwb0WVx9gpta2L4RNuEel/YYVtOcfsUz&#10;kwtXug4u98tVEa4QDCRLhtbT6YSh74HMiDGiaTpIXQyohq6su69jl70kLbuUrvNNip5s/HT0dB5U&#10;k6Nf8ORV+VDVwwSAIVotu1CcRAuBFy1FFnDB2c2AzlRU02GOfTAmJTeoOzShOd5uP4d93M1UGWPE&#10;crnCdrvFdnuF1WqFzCxzMY4icfsekruWlWmfp7SuZiRTE0kF5oBK2zGdy7Nxr55HXJtgZ/Nr92Gc&#10;tenS4caUGvVMHi5aG3NeN+227ZsAktD4TMCYkE49xlMPJDPbWr4KTREAePkFjzZyMEETjaUOYEXv&#10;qcyvOZAbndMq2mIyUUEkA+CAgAiiRigKS0ST0HIosFBtBRg0eelahZr7jK/Ru8f1vuN9fS6+q6Oi&#10;ZrBFEokQM4Nf32F89hL86g3aJOA+s6ViEPqT1NwVX37xxdNXr1/hyaNHuLq6cq95kSzVMzrJBstj&#10;Ag8Jd69fIw1Jkjd1ndiAybxJqw1iTZyh+UL77gEXEzrw4YNozM+OonoTpgCt9YDMaJpGMwrK4Ig3&#10;etQcEYW5CcMR9Y8n5Kqmo267aAHq/rAOo+TEyNkAnI6Xp0zWRRmslHyQBW1OlmdPu9B3wFPrggkR&#10;Ot6ckfOINEo5d84jQohoG030FWQDtrHVtgTVuBQHyLPxNZBIwhgDhBiMKSOrxpGJsFgusN1uJY34&#10;9gopZfT9CYfjEaTMwGrGkOA8mCeOOSUKWCgRN8ZsJpxcAa5IEwluUkEuEqWOJ1U+D3OyX7QOU/2N&#10;EGqqziqjXrMrPiPatfW1mnc7wwCGRax4EigTy63vssnTMOB4OOJ0OGAckyZ5CwJOY+NOntJcmrx/&#10;6DGRfO1d552rc1D3l3kSWmjOwWSb33cU3PyXoU6goWSxcH8nQP2kGJGLUznVgCUEnxvzOBGAELwg&#10;mj1wCiSmc0VEAvCVRhCJCVGbghjF7Pfo0UPs93sAwOl0dO3bmEbZg1A5m6vVQWX5zMdu3o7yuRrb&#10;ak7rMT9/FdB38bo6Z0d1cHXe2VHRANab1uYSXycheNVc83OIkFej75QYaRjQn3oMfQ/mUjEYJGYQ&#10;863QAAUJi0TRJoCgDp3muwNPHSA+PGWNuIZWUwLEGKR4YGgQKYqAzBDHY6UhQj5ZtGaVloK0PTWw&#10;mI8LYFFx9++593XcvO/4rjUXvkeVVzdECP2I4dlLDF+9RDwOaJKAvqz/AC4pDJgQf//7P3y6Xizx&#10;1//ab2O9XMqGgkhKsW0xckKIEV3XYRwTxkEiSfrjEXkYRUpoWyCao5ZtflvI8A1971jrQM1jneWn&#10;Dx3EagfnIlVae6RtQVQ3YxJ1jjoXOiEyyYUkfDUqmDBg4BJB2bUwpFqaK4uMtWOWPtnwTcpZy7ND&#10;mbsUC8sm0TtRVL8IJer1MJ6FQMHARRQ7PLIgzqg6kJyBPCANA5DHAgKZQUwCFmODEBugihYosmVh&#10;Mi7Zu6bBzjNsQBppIpszhoDVZoWb2wdYbVZIOeF4OiLlpISo3CJQA6jPRHAfCuEkla96cSx0EczU&#10;leZC6G65fn+yKAfY5p5qMqwNPsbVeIPZp8Hv57+LCcc0F5M2Mpd7EMrYTca26KjsRAqqtTFzjFLB&#10;gABkRn864XB3wOnUg1BqlHjkid3HV+Plw853R2kUZjE5DxY1VUbHAGANsESKg4cXlpboS30rZEjJ&#10;q62mQEihpBFn05AxIzAjgouPBaE8w8E+GbYEoCA0SLIyixabbtSp0GOHmENZ9n7UyCiL7FGw0bYd&#10;rq42uN7tsFwuMIwDjqej19jxdGO+SbmgTsjzC/MvT7/P32UyftV17yXlVu+lrzR5Xq3JqMGCHGHS&#10;RgOIE3Bu9JOCGzItQsRBljnSa9/zmDAMPYbTEWkYEViidcwFIlBwv5oISZFv+Ymsxk02584ayDDQ&#10;sEUJBgFGlckmhKgaa4mCDCSOvRJWnZBCxhhFQIIKMWaGsdBMkOhkinYF6gtiAQXTubs4L3/FwQWM&#10;TkL6Tocepy9fIL14jXYQDaJQWwMXANTUxADif/VP/unT7330ER7c3IKYJQFSFjXX3emIf/vT/xtf&#10;PH+GxdUGTdtqXHhGHkacDkccjydkZrRti9i2U8QMI20Vob2vM3Q/8fuwgaSyOcowFSYKqG1UQERO&#10;AjCEKZvEI20JyhSbRs0kigwyz0lm3VO4lG3SjnttGGMmq2dQ+YII9J4RGtWiNFofxaRZ4+vz/tnj&#10;YaGRyuyzOOkGMBoiqTcyJqRxAKsXdxOCSnyaoEg9uKXLedImD0MFFASZ5A0HFKbxUUwAkICo2DbY&#10;7rbYXG3QLToMacTxeAQjS0VKEIgbEEdxbGQNg7VN7X4Q5myqfTZPeAUXNh7Cd0zqKnb3wm5rfZCZ&#10;IKyv1Vyg6B9IQfRco+Maponaqjh4Eipggarx07uAmRyUcJb07TGII5sxL/NtAgN93+NwOCKPCSEG&#10;tItWNDXVPvDHXdhKplmr+zeXuli1EdMIpsKgbJCICrCwhxrDJy4OmOZ0bVEkiQRcjLo/Sir9UiHY&#10;JEa5rlRrtTgTyvrS62yXumChIeh1xk7bLXV/5drs6xhgLfUuf9v9Q4xYrVa43u+w3V4hNgFpHDH2&#10;kgq5wmBlXO8hcnNQcR/AmH55+V6X7m2nn/11YS7JiKBdS+V8+S7MrlH/hjrBmYWwG2F3wYhVADVA&#10;o7So78HjCB4TpMq1OVZCakfpPjbzWgb539ZES6sWADQk4MJ0GBzEyTSE6G00p+i2iWgjoaEE8IBE&#10;I1JMCixYgUVA4Op+ENDC1YjaufeN/9vm5kOP7xJcTFJIEBAyI44ZdHfC8OVz5Fd3aDOALNph0XDo&#10;SJhTJ2fEf/ZH/+jp1WqDCAISo/FBBP7y81/if/xf/mf825//GX7561/h+ZvXiIsWq80G4ziCGRhH&#10;qY+QTgO60KANjTtM1qrkkkrXVIzm8FgILqEUHpsv+vcalEofTWU1u6qsJrK+Z2w41aadc9KIEm1r&#10;pcVoYkTbNEXOZIvNxmSjm+p2ovauNqlI9nJGZsaQJTeGhK3Kgi/IUf1AdFxNMcgWimibuhqHDFKH&#10;VWuXyBKBLOmW1v9ILAmCxhGcsjhHhQCKjYQ9qm2yiUFNZBK/TKrmrqMHWJmYEw5g6vFOQmyYpD/r&#10;zQb7/R5X2y2IgOMw4DQM4AwERH0V50rROmmwoknK1gYFZnZ/mbMCiCx7H1f3g8+QBbHpmLOAJUv9&#10;PfG1ICio0DVkgKFaC7Xawwgp0WR5lPN8GxtoNOCk2g9FT4GqYFjVMgnzUtNZZoyDZPg83N2BWbRB&#10;bdMKMWV5kr1bXwDbMxZCKSGVcZY6elIjBtPOTACGDm1tFrI4ncgWaVQ0HRaOyqH4TGSg+GewjV2l&#10;FTHgYvTD5xIImWbtc2IAA47BHP203UWJxH6u0Cab2XI7A5VQoGJEZLFYYLfbYrfbYbfbgjhIhESv&#10;BQbrjKPqgxNIfADgAo22J2enmVR6cPHwsSGfmXNtpq/F+roCDibP0O94cm6tYamvIz/f3t0Hy4GF&#10;HSX/g+W1YB3nQOqUmRncjxgPR+S+B6WMhoEmiMlWssBCU1Bbg4oW2XdpIFVUSBtkM+v+9hBpA0Pi&#10;5LmIjHUEdm3AzarDsiVwHoFxAKWsPmcRojtrAGoAyF5RcQwBDKsrBF0/b+Nf7+Jttdmk5oX3XXcJ&#10;nH4o/5xeq36IUC3QkDA+e4XTl88QTiMaFj6SjXapcCnllETAiv/4D/7u06BZIiXfhTCHzIxnL1/g&#10;//jp/4Vfv/gKP//8M/z8s8/wi1/+JZ69eIHbhw9xtdkg9QPGU4/xcASfRhADzXKB2LbOAOtCM64i&#10;LuKhEzZY+NrXBGWGhDHbULUcVm84RgEZrFJiSbSkyVtIF49644szUJzeAxqi6o6UtvarrasPcolU&#10;PycwxiT5MBIndewMJQdEKMQgaMIgXy+5qKLkjSV+3E4IxdOXnAyrxkFHhVnCZTllkAGWplFJgBED&#10;3OmVwappYXe0KkSCnOkGk1Snk+NDYUmN2sUC290W2+sdmkWH0yAZYs1GK/Jj8nkCKbAgk45k01st&#10;Fih4aKJKKKYI5+DETvquEgjbiECJuUlTCcjJmepE40bVFwaKnfqak6g7jhhf82OiuJj+NCXGRX5Q&#10;wmXgg1xcm7A/BjhlnE5HvLl7Dc4ZbdepWnpKpCrcPVVmOEHyG0+ARXDGO+vDHGDgfibnA0ZTDYU7&#10;wsHs3yoBodyAFWWxJXLSOSU3l1UgZPY424IgSIp88xvydanGj1xMP7UG0PoY1PnbNXX6W9u2Ugxt&#10;t8PN/gYxRBwOd+jHE6QXqoWJwYUB153UY2xZkOv5sjm6xEAA73Pd6RoElHGZfzedIz9qocDvdd4e&#10;+2vC+CpaV2tBLGlWGW8DQ+IX1jAQEwNjBvcDcj8AythDEyU3RhOKk7DykUhmjhFQIz5vkHXio0Mg&#10;WH4fCYONRIic0CJhHYDbRcST9RIfb9e4WS2xDAFNytKeXOVpoSiaRVYnVSIEYjAniRIMStvPR/WD&#10;j2/LbPKh9ymJ0oSftQyEwwnHL59hfPEKUR05NS4SMPqkWaNN+I7/6R/83aclmaHZUBgjMrrlAo8/&#10;eoKbm1uEEPD69Wt89tlneH044Pd/73exWa+RR7GV0ZgxvDngeDrhkJKDhq7rkFL2abYkSbIvDdG6&#10;XFYYyTcY1Pm0OoGvfqi/82JNLNCLNVEYQZB1Expx7tRL2kbzUtjGyrnUIYB7CHiSKo97tofaZlBa&#10;LIKoZPXMViMFJBVqPWGMbJ4YJL1tXazHok0CMShU9m7n54VZAZh6n7NqZ9QRZxwGmEe0ZbkrzRYT&#10;EVdynYAQwLKrxkBul6xVwDa3hVlI+2LTYLO9wm5/jXa5QIgBhzevJNMoj+J13sDNMOKwqPd0p6ks&#10;5bJZiFEes2ggcgBz0HDFUCSrbHkIAEICaU4NQkYg8dWIVPm+GJHMJunDr6/Xk0xpiWQRoGvsCQoO&#10;5P5eawQ8qePmZq9aba/nWaiqffLPtn51HlJK6E8nHA8npEFS+S+6Do2lbSdy016MAiRzFlVmCMHn&#10;rRT7MqZC1fqZgQdtC4E8bbCrvkFuJ59HjpR1SA7MZf3LP/HHEGdx81tyyZtl/QbfyLJHVJ6qmKO1&#10;V/95CHZR46ttzzVbpjWRIzjDrA9GkZgNOLRdi6vNFXa7a2y3V5rh84h+6HUdcAn7NgbpghecJl40&#10;PTkDx1lb6nm6V1qu5kpAwPnvtcBVaEx9/zBri9yxlGoH3F9LvzPfKKN59mjTyFkESMNq288ZPCaM&#10;fS/+YcwKyqK2ybZgieqxkFegAhcVvbV9HAlowOg4Y8UZu0h42DV4slrg8bLDbdvipltg3y5w1XTo&#10;1Ll6TAkpC7uVMdKIJNa8GepF6jysiJhf6/g6wGKiYfwmTFRuIGuTgWZkjC9e4/jFM/DhKCAQrJlM&#10;le/p3nfyBUb8x3/7P3qqLZM3MMZA4EigGHCz3+Pxo4f4waffw8ePHmN3tcXHH32MH33/B1ItUBlT&#10;FxpEBLx4/Rp344hEhJSSRl1ET/zDeUqwmK1KKAsDCEXq+9aOakFz9W4/Wf9JR4YVhbmqmqnSHJBn&#10;BSzMp9wDXNTKfu+KMMMIBwqqt83KpEXCtJz7aBnsdANRKOo8UfF+Sm44AAAgAElEQVSK/RAG1ogr&#10;r3t5XJgsMiWalZwnG5wUeUp++DElgJMUGNL2BoLnAmnc/0RVuJk9G2iM0TNyW39tzAsvlCgdkPie&#10;MBEWqxW2ux0ePLxFEwAKGYfhiMSSNMqHV01BKQ1gHhEDIwYGMIKQEELGdr1A2xACZXDukVMP5hHM&#10;I8AjOI8gYoQAhCBmB0ukpfGchfhSPXrywSI7fGqrZSYLiFzTYMTc+j1d2eaHAb+f3wLz7+TvUkmF&#10;J+dWEAYxBPHDuDugP5wwDuIDEEj9h8xvBoCXK6/CZUt8f+ncfSrXOQMkkDu++doLUxs5K+F346E6&#10;eMqrDHlCKYqWIGpx09TYPzO1mNTqFR0n7a3mxwAdoRQU1DolBnYKqCqMFNXcU3Vf63NWrWdQwLJc&#10;LnBzu8f+do/FcoHMGf3Yo3dTiTAna1Ntqrmooag1BhXNuHTOfI7Oj4ogXpzPmTnK56Se52oeqMyF&#10;MKUCxqSdBY0awLBWFS8GXTdQmkUMThK2OvQ98iA1m8RBPXo+DNtApTaUTZcAyKB8JxAQOaPNGcuc&#10;sCXg0aLDR6sVHq8WuGkiNsxYcsYiEzaxxa5b4WqxwqJtEEEllQHMsTtr2QYxFYvzKGtStG8GLj70&#10;OKtB8w3AhZt0oCb+04jjl89x/Oo5Qj+ixHppoVBmcdBmFSAAABnxH/7t//BpRaeQCBhIlNEmRS9i&#10;h+1mg4f7PT559ASfPv4Im9VK1d8SfhJixFfPnuF/+zf/Bn/22WcYUsIwjMicsb3aIISAlEYEgoY3&#10;KrM0klij4lniqm9t4KzjPgD+NWyjuETLACybm5kLgtUIYXV4lM9NDAq04NKHlXT3nakL3j6yqpUm&#10;jVDim5kx5oQhJzXTQG2JhaC405S2Q/CBghPtX8mALP4KLuBqyJdtfjNpGEMLAPLQY+xPQE6IkVyV&#10;CGLx6IZIC+b5DdUiiHRszEPnz4iYJchR04+BS4mYiWi6FsvVAg8e3mC734JixKE/4Xg6QSw+wrRD&#10;ZARKiCEj0gjmA9qYcLNf4pMnW/z+33iCjz5e4Pa2xfU1YbNmdF0GqAenE8CjAgmAMgEcEahBRItA&#10;jQ5xVqCZJdGOFJ9AKSlf+qdfOcM3sDBfpecGBR0fn5gCWoAS1VL9Kgmpqs8l7h6ukcopCchl+ft4&#10;OODN69cYU1IH5VbGP0iF1VwxU5cMa4n5wjH/vmZ+5mdBNaAgGTUzo8mCI2+3keKgSNT8MXLQF2QK&#10;EkvIoD2LqOyLbMACJk8buEOZL92b1aKE1cwgzbsiDDMUOlH+876ahEYqTc9NG5Jci7DdbXH74Abb&#10;3RZNI2nb+/6kmsbi22IapRDM96eAoQJtZnMgjbmgXaBJv/2cCdioNA80vaeP63zsqnYUAYlgmUkn&#10;zyAzPhaXR1XCVoCiaC+Cq0yUHhChIQEcPCSkYw/0I7gfhZlp3grxWwGQk+f2iErPiDQtPBiRRyyR&#10;cRUIt22DJ8sFniyXeNS12AZCl7Kq+y0iidCFiHXTYrdYYrdYYhEieByQUo/MCTmwCuGqRcvi+Ong&#10;6q/Q8S6t1uxsGUcQwpjAb444/Por9C9fIagVwkE8i3LBeI35TAFA/E/+1n/wtMolItIBQbJVhoD+&#10;JKW8SZ0914sllrFFZAJp4au2aTHkEf/nT3+K/+l//1/xs7/4Bf7sF3+Bz/7yMzx//hycRiwXHRZd&#10;i7ZtJaMdKjOIqSZ9GfO8r2cD9SGH2+lm77avavuw5IgwIgUhRpwrjUthkhSCI+kYSyrbpHks2IqR&#10;aSdsu4qm3LQitYmBXfqShCyCyFNOSFkBjWXXrJiAEUXrG3sCubKxXT0NQByc4IzMNQvahjYAGMXR&#10;M6ckamrbuK49kQRkTYieV0MAGUMKTEGZS01wyhibc1tNIFJKSJywWC6w3m6x3e/RLVYYRokoySlJ&#10;+vR0AmFApAExnLDqRjx6uMLv/PbH+P3f+xgff7rAw0cRjx+v8OjRGg8frrHbd9isGqyXEctlg0Xb&#10;oAnqK6MRGYa+iErYIVDZdEmZORUYSobK1FwySaJVLd2y5DSa5F1ruJLgi5MjK/isHkHKRLmsMGjm&#10;SmfwScxdfd/j7u4g8xcj2lZy1NROsxWNf+cx3YdFOvXYHDdllJEou6D8XWsrTMplQBw9AZh2RRxy&#10;FYD7oBpDEjWt7Gd2pmb3nSdFc+AAqgivAowQEbSwWtbke9ZfE4BMqUmhyr0AAx5ZmF8je7JpWuz3&#10;17i+vsZqtRJnz2HAOAwCAl0zZy2yVk41Fjxp67k2Yw4i7mMmcwBiDyzfcz202pJybhkH+Tug0L6a&#10;3ng0m/rEWEipm78md5b2Q01GBEZEkNwYGQhjAkatU9IPssaDVMiNGqFmwMJq6xAzYma0nLFCxr6J&#10;eLRs8WS9xMOuxY4IKzCalEGZ9R5mImNQzmiYsQoB267DpmuwaANiADISRh6RNBkhEYFy0CgoHcHf&#10;EL54W4VUO+7TQt5zMogkehCnHv3zlzh++Rz5cJRoGM3/AS4RMoFN+CFjPoj/8d/6g6cW1mWbMzKA&#10;YfRwMUtJDbCo8nIJaUSSmKsxJ5zGEaeU8ObNAX/+i7/A57/6HL/4xc/x+S9/id1uh8VCMgk2bTOB&#10;D0ZOPF740gaqPr8rAYk7E1aUvZainYgCmuPBNgn7ZgAq4gxo5UQLSSsRLQCKmUJVrFIuPRTp3dqq&#10;XNzUn0F3MCljIj0l6SazmG4BF1lVfuTEhFQHaOaZ0Cgj5Krwjw2ASUgywzom8n02lbhGkwQWT+0Q&#10;xKY4DAMGtRmHKOXmJTNkoyGS5IQl5QwOQc0y5OrqeqNJOLNxyPJOIFAjHt2JIharNfY3D3C9uwYh&#10;oO9PSMMRAQMCTlgtMp48WuN3fvsxfvd3P8b3P73GdktYLEd0XUbbEZaLgKvNEvv9FR493OOjj2/x&#10;4MEVHtxusN122GwC1iug7QaEcATjpL5JpNoS870oXiu+Ds80EQoWyeCiLT+anCLfCRqr63PQhSvY&#10;12/xh8r+iwFUVGPJ7oNA9X1YKhZbnZdhGIQoatZCq5eSLbX6PVqWdx0GCjxlt61RQBMh0cTh1yKK&#10;BCAUAMVksZxlbRHEdOFZHImRgzqVKaAI9j5r1yUzCVCeC0CdSiXnjGjJSp9qKX0+Ngyu1oJqtzR2&#10;VhxEM2LTYLVa4mp7he2V0MKcEsZhFM2YtjFasi/CZfpnc3sJNNhrBirmJpRyZvWpBhszjY+vJTLQ&#10;ECogo6t10tay+gw4WgiymUAmq5PUL02jvUSbodIwW0ReUH8cRh5Eg4GUAfPn0zBYVnAQENACWIeA&#10;6ybiQdfg0bLF42WHfROxQkLDIyiLD5T4bEQwBCBIH1T4QAaFjLYNuFp12K4WWMaImDOoT4p0IzIF&#10;jKiqtf4GjvcBFsB7AIrZuUTCR9LdAXdfPEP//BWaJDwoG69mDdXNUuOl9qsiAPHHf/PvPK1VGYFZ&#10;HDaS1ghRZgINbcxJMgKy6iiF2WQgBKw2G3zy6ad48uRjPLi9BeeMF8+eo4sN/sbv/R5yGjH0vdrA&#10;AhaLJZquA0MczFgXZfCgQ/LvfIP4nje0ez6PrgOpwMWlgSagbGQHJDRlflRtOZbFllPxVDZ0b/bG&#10;ECJi14mqzK+pNqtpE6iybcvdYZI+a0ieia+k451yktTZlUe7mcxBBMSgefTN2cjUrTIqBnQ8LFbb&#10;b42QTaZZPdUcY863OTOGcSwgw/xNABAFtFrUDkQYMsDq1OXZ7fSo1b9uqoGpiKV9iSwctkHTtNhd&#10;7fDw5gbrZYvIAyKOePRghd/64QP8td96jB/+YI9HD1dYLxMonEBIPrnWp0UrGov1usX+eo39foXb&#10;B2s8fnyFJ0+2ePh4jevriN1ugdX6Sut2DEjphJxOyGpKIbWvWqhqtRJ1fs32LOvQQxwNRFTEgKv/&#10;PYwNcC3EpcXrei6dL1vDik/VX6i6Ss2WzjqYMQwjjnd3OBwOyInRxgZt08DMIlAmX4CfLZdKUzPb&#10;Uw6CqBAZ5x0wB8+ipagZWX2dXMtOoEQCFaoAGGAnd4IuoXAVALDGVA2YMN2KERp9sU3p/xhei8eq&#10;Ftv8sQkjZubT/pcqm+TngUr4s5mkttstHjx4gOvdDkTA6SR+MT6X9VhX7XYG8RY+MTeR+PfTs86/&#10;8XOn5rz5mHk4cf0vBB/7MoplzN38odea3DFpjwIyE+cC2KOTzPRVyrEDUH+Mse+lqCYR2iaqbwWj&#10;y4w1EW6XHT5er/F4tcRt22AXAxbMiDkZRYQ59DLEAVw0gQr+A6RQme79LhDWXYvrxRK7doklRWDI&#10;QhuJkKKtlXOz6Ldx1E7W7wss7Pz3P7mUvU9vDnj966+QX9+hAwm4YxkXYqvzIzTL6gABGmn49//m&#10;33lapCnZ3QIiLaRRVIO+9PRvl3aDec8KulyulrjZ3+D7n3yCT598hO9/9DF+50e/hU8fP5GIksMJ&#10;43FAGjNibPGzP/85hsTY3T5As1ggjQk0ZjRkURBZ621IyCbFqHnMA1hDV4Xgsdv0MmzDF32rgYDC&#10;yJWIG2PWBW4ovI6vsKqFDkY0msQYdwzRfQ+slHYT/x/23qxJluTK7/sd98jM2pd7by9ooLsBDAbL&#10;LOSQI4I06YGiqA+GjyEzPckk09D0oi9AM3Le9ECTUSI1mMEAaHJIamZ6uWtVLhHh7no457h7ZNXt&#10;RgNczETGtbxVlRkZ4XHc/Zz/2QNRdLEXcm2J7Td1S4iDhFKnxEHRkkm6KTjnxJQ0OyLbg2nKqlSm&#10;O3jwZ1QLgkamQ22rjmndJVdQFUS6ctrWU6EKMQ1UImVymrR4Ctnat2vArjKYAHGFyAqRQd1CxTIx&#10;pIDk1k2yBAzG1HQu/K5W3ZNckJzYDJHLsw03Fxue3qw5P0l89K1rPvzwmneenXB6lokyghyQ4FU9&#10;jMm6EDG9HxJDhGEQNieB87OBy4s1Vzcbntye8s67Nzx9cs2T23OuLgJnJ7BZF1ZDQsoI6WDt7M33&#10;KzCINu3DNl8FF7Y+SwV5aiGoLCFQ6QJakEawObECPcq0u7VrQbRivhEXwFStuuoWvlhxV0PwNZ8g&#10;j4npMDHvJ4p1qlwPA3GI1Q2j6bsenS91R/SgCmgppUiju6/cBZhoQYtLu0q7WMvG6QWgC/8FHLN1&#10;LdZxNTBJYK7XhlqZUbprCPU5nC49/m201rMUXAwWfCn0rkw6EKgtAfT6fZv4xdObDF2tBjanGy4v&#10;zrm8umC9WTHNE4fDjpRTRyO9bl0/SBWEPgvOuvqsFerYaH9USvp16F5Sv9tA5BFQoT1rvVb/XX9J&#10;m9ne/VKtQz4VFSw5pTzTTtPfNfnDXBXWvbRamIO6VkpKMM2EOTGkzHrOnCa4lMKTdeC9sw3vnZ1y&#10;u1lxGSOnImqRrWA71H1WV7U0zu/ruj6Pa+oI6xg4X624Xp9wGgKS1aU7lYlcUtcColm0/LmXllxX&#10;Pho9OKLzkuaPW60W59DJNr+LPLRFGgUe7EXBLE1zYv/iNXefPUcOE0MuGnPh1v7smWxoCq5niBut&#10;4n/zO3/zJw8eyAjuQe/Lh2vamDchcpkkpqUHgZP1mpuLS969fcI7VzesRBvXlCkxT4nd/Y4/+dM/&#10;43/9o3/EP/+X/w+v7+/JCFeXV1ycnGgGAkqhZJUsh2GtGkIYzGTdgii1Z4SeJ8F65VVGsTTzPZy+&#10;xoBcH3WXQVv8HaM0BuMBrxWsdBvTXRhDbL07NLpYB1yZt5WnLUUqWGtmyY492M53PjhTSFZpUK1y&#10;iphDiDWjQmLTrhrPMeboY+xoIbTrlxp2rd9yi0vA6oGkmezWLRNsqmFEMytHPbdkSkmG/Kk1TcT9&#10;ouL0zJWG2egbSmYlmZUkVrJnHfecbxJXF4nbm8jFGazXM8hEIZnWF1odhCZN2q+i84AkRHJ9xaGw&#10;WQdOTgcuzgZurgaePj3hvfev+Mb7N7z79JKb61Our085Ox04OYlEUbCV0wjFwYaZESo0oAohb4nu&#10;S06zR0ptKFeFoLlkcnH6uTXC/dVSN3VTDJxJNbPFMhS0VKYQMtb9MZCmmf1ux3gYdV0EQVYDmHvN&#10;3S5SN3/vAqAyYRVw1d5YrRR9PEVdzQ808O7vxxjs8emVKTVLn4ILIbkiYP+5ub1noceZBb7ufWjV&#10;qoFr6+oWc3Ak3WjKgh6lBU0vsgU6xi1Yy+/MajVwcXnBze0Nl5dnxBg17ijNap3shLCX1i6lu56D&#10;gRrsrXywj79xIe807wV+fxxbOR4DE/6p87j6e0c3rydTaVvBnR1d8TGpsRLU74TuNA2Yj+0eNibn&#10;XasQWIkQU4LdnmGcuR4GPrg+54ObM969OOV6s+KEwlBjA0yQt1FSK/jWgGBbE7Z+xPZbrHTSeIw1&#10;wsUwcLZecXqyJm4ih5I4TBO5mIIXfC2VxfP3IMvp+Jhk+nWOxVW6JdiL8qpoLNZCW9mxFMp+ZPfF&#10;S/YvXzHMiZC05Hd0YGHgj1BagGcnK+J//aO/8ZN66U6reGhwae86YTxXuz/DhW9OqcZmRAm1QJeg&#10;FolxmviTn/6Uf/LHf8yff/Kv+Pmf/5yf/smf8u/+7V8wjRO3tzfmD4466KIoK88zqzAwz4lVjEhQ&#10;X7LiCWnR3g4upI74K6bDSU5Flv27cnS2+5xSzswp1fLAMYTaLVZAy8uuBi0Cg3dZbDzUN2l1nYjU&#10;yTl+VXRrCzznpAW4StJxxaj54LlnSoNaGGIrppStIVNbz86YpAoKoAYnIcfjUYGY5ok0mxVDsC6z&#10;Wl55iIFhZe4Z8DIYaL0J5y+qWSsoFaQMNTh0JbAJiRUHYrkjzC+Q9JyQX3B+MrIeJgITMNv0Dmot&#10;kei27qNZe+wwSWDaiubiZ4aYGVaJ9QmcnQ9cXm64vjnjybNL3n3vmnfeueLZO5fc3J5xeb3m4jyy&#10;2RSC7CnlUDN8AuYCqlYFqcKh1DUaCAwgESk69poUVNUCodSXWexK6XZjsmJjqjU5OPHPa00F+17A&#10;K5nqepzmid1uz2G/5zCNxrxF+8z0ANPWnttFlD/7irBzuj0i/S9HU1HjnqoG0E4qx9fo/vbvNjdp&#10;u38N8kSZnTfA6kVJoFixH+dX0o3a3Dm2+Uu7fBPKVZiHSl1NX8bmvI28Wnhc6HcP00B/YbPZcHNz&#10;w/XNNecX51DgcBiZphEJwhCtkVrWQFFCJ6ygBZgjeMxPi1sL9fl798aD3xd07megAZSHrpojd4hP&#10;CVLHUEGNfyd09zagIFhDM/uOVx1W97ROrBYyNJoWtW4EKUQyQ4CL0xPefXrLN99/h2+8c8vtxRln&#10;64FBROMqSoO+3Yhw/qZvuUXO1pStS8Fp1da0iLv6hGG1YXNxQTg/YwzCIaUWR9NZhx8DcE44H9Wy&#10;vsqXH2+zYLwNXNRfy9E5dfL0p2aJFA6v77h//px0vyPOuQZuUpbrr/IhVyQM4C/AxYOBPRh0d0Gz&#10;Uiz8PiLN95uUqIOnmybbgCYYQ4ys1isuLy8YVitefP4Fn/ziE37x81/y4uVz/u7f+zFn52cIQpon&#10;ViGaHBB1qQQvSa0BjxKsZgKFFovcaxTdZnwwKbL8KY++W38WEw2I/p6TVfZ0y40J8iha8KqaCi3D&#10;om+h7WZg3W9ecfCIOdejaYb+dNlKsOZSSAKUwioM9X7avMeLzwQraa5HztnkvdQWyZoN0YKipGcC&#10;Fdd4AFQxC8YI86zFcYMaUaJkAxtW48SEvrvVPCyvIl3RKO9I4UQKJ0wMZUuYXsP8BTI9R/JLYn5D&#10;lANSRi3RWwIi2m+xyGAm6dIYxtc+MjADE5mZwgwkYkwMa9icRi4u11zebLi9PefZOxc8e+ecm9sN&#10;F5eRi4s1682azWYgBhX4pUxYKSisODveNVGLQA2EEsHARSmz0cYCajo7qru3lOe6RLPoAzEE54Uk&#10;6K0X3SoKQYtmeXptCGQU8G+3W8bDnlAK62HNYLEYGixnwKJWyrT14pYZv8HR/jl6+8E7NevG3jsG&#10;KF/Gkx67biqWCh6agwhoa7kTwD2gqdlkTjc3Q3SDKBbgKx6ojAss4wkl1I3yqAnb+JAGS6v10+te&#10;nJ2ecXt7y9npKSFEba0wHphNGaixYA5whBobVgvEIfVfT5nHFMYFfRegw//u3sf406MAg6P3pJ3b&#10;0z44gKABCVoch1vlghWoCoSagebgYjCAEQIMAVaDcHtzyUcffcC3P/6Qd999wtnpmhjNSJDNUlsp&#10;88iaEdo8swQX1Z1QieeAUT/M2crWrzeMq8hWYCqCZ5ykOYFlDwVbXHVFLtb8EcD7DQDGY+DieC9V&#10;vcWkWQVSoinscpi5f/6Cuy9eIOPEkDIDqOHA0xEtNqqBsv5eQvx7P/z9nywJ9uUbWTdhWfCrB3Uo&#10;vMuoaaE5e8U86kQWCqenp7z//vt8/OGHfPfjb3Nzeck8T3z44Tf5h//9PyTGQJonzTCJQ/Xvr9cb&#10;VsOa7X4PAnFQK4VXGPQV0dO93ywPp+FXAxftd9P66eBgQXvZ52SL2QWwf6i52atBO1eCGCBpNFHa&#10;l2op8awV6e6r9LZn85LcWIXPnJjnxGCgq7lddLMHUf9lqKl9NndS1GKBB6EZiBFnSU2/a5pDYBCQ&#10;khRgzBOkGfLEKkbIiVCyMQGxuAwhMlA0SkpTQNFCXTFkgsys8p51umeYXyPjS2R8QUgviemOlewZ&#10;mCGZCwKtT1EsCDGLr7PSMduvexQKll4W3BwOWBVPb+EeQmYYCuu1cHYaubxe8/TpOe+9f8N7717z&#10;zrsX3NysubgUzs5gtUmEOBFkpuQDYp03F+KgRLBspKqJ4hHryYRKJhcFPKWiUI10JwdKGVTMl5bV&#10;4ExyuW11EVWzfrBw1ZSYdgfyODJPkzZDC4FhvYIYNPUuBnXfmYVE6wt4EHHL+jq2AD7kcDTLBU0w&#10;uaXyAbh423QebeMGLLTLcCluMdKTmxtB+UQ1qtV7GKGOXB4+qGAxTsrj+kJbbvmUxVgfG7b3dIkx&#10;amyUAZ71as35+TlXV1ecXZwhQZimUYVUd6HKX+s/FcZBvMKqLP51T0/d2/1rQcYlQds5zb30IB2e&#10;3lpid+yEjuDuj85CQfcMQv07EsxtZ52aB7PI4oGhmZPNwJPbKz54/10+/ugbfOP9Z1xfnrJZBYLk&#10;GnBdga8o7yq1SWX/iC7s+3UYmjXM1wM1nBoQ7fcUBsqw4kDg9TzxZp6YS2kWY1Ewi4FIHuzDfl6a&#10;nPo6x6N8Tqp+0ebgyGKxsPZVC5MQUyZvNZBz/+oNQ9Ikj2g0cBduqcXWzPLbWS4QiH/3h7//k+Oh&#10;HSt9vVbhw1qiuXae9+iItmiylcau6W7SNC4RFbbnp2e88/QZH37jA77z0cf84Ae/zdPbJ+pXi56F&#10;EPj008/4J3/8x+z2e84vLlhvNpYma7X8StHy0OafrubCCiyOJ+Ixh8eDHw+ZgzElBzBiwlowgFFr&#10;QyjDiJ4/b4yodv2LgzGWTEpWRlWaP6wuuWPu6s8QsIZPOh+pFEpSl1QBWplxfbW28lItCgikkqz1&#10;uQpUOVqVDpHUR2dj9M2LVs0LOUNOTONBYwjm2fym/vxClIEhrhQUMBAwCwcTQfaEcs+QXjOMLwjj&#10;CyS9IKTXrNgxcGAo6n7RvPmBEFZAqGVoS5jR4NHlZv26h35Xhaf3uHCGrFYn65gYFGRIyAwDrNeB&#10;k5PI2Vnk8mrgyZNT3n3vgnffveLZO+c8uT3j9uaUi4s1p6cDQ8xImSllQquLGtPD409882bdyGSK&#10;BX6yMLMHpEQFbdmBYcu48PXh68ddeCIaz5SSzb25L2Mp5MPEfrtjt9vp50GIw6BuSmeAXXCjV5t0&#10;BrqonfIV9H7MxnRshv+VLoQKdq0R0+Kn+lR7reej+6HGlQEtpFroXRhLXaR/09eDWQXDkpdUyy4+&#10;h+1ZvVaM/121epR/DhbweX1zxbNnTzg9OWWaJg6HA800rKP2WIymDPegoLc0ND74GB0fgI0j+ler&#10;j0MP6eK5jkHK4jtHMEd6C4VngDSwFEUL8Ykpa1H6BpKF9RC4ujrnG++/y8cffsDHH37Ae2atCMH4&#10;gwu8DjMYdYGykAmL1Vf5vme6uQXrCHhJC/QNwwpWa3al8HI88GYambM+zzAMNYvO90upe0Y6Rf7X&#10;51W/yvEYsPCjj+Wr5x5mxhev2X7xknm7ZyiaObJok7HIJV/exx6G+OPv/95P3HDaazX9eybn2sD8&#10;zZqk3n6UUqwgiX6hbTCd0GI3RqRVvgRImZNhxc3lNWebE+5fvWKQwHq9Jg4D4zzzf/yzf8b/8D/+&#10;T/xf//JP+ORff8LusGezWbNZrzk73ZAO47L09oKS/aZ7y0aq32nP9Bhvq89RhY6hbnzfJxWwXhK7&#10;34gWNBejlmKOFlPiVRpd86hMwMZTup8ibeG3+WmM0ksRq47dGH0DONqjY7BgqYrMRUAauPDndT9y&#10;jeg2X2gxc2y0DqpYoOc0jZANXMSWghZVJ2GQFesQiSQGRkJ6g6TXZqF4znp+ScyvkbJlJSMDiVAS&#10;IYu1YR+AiBS33CQkJCTMhFCgpi7+GkcxKwARRFuoBfHf3d0glTlgwaiVIKUQI8Qhs4rCehM4Oxu4&#10;vFxxc3PK02fnPLk95enTc54+OeXmds3VVWRzkhC5J6V7zUQiQ9HOGs58PWDadciqTVWmZTn5zdjZ&#10;aV+mQYHWp+nWHF4joEDJhUE3LmlOTOPIfn9gHA8ADB4DhVVCNP9q6zJamqWk3zjd6wGDe2ClONK4&#10;ewH52JQd/d6ApQame0fNJBoIPYdipcR9nbdn0NiuJSNu2SX28ht2xfTEXYgWkO0xL5V3dnu5H2vf&#10;DVf5Sqka4Xq94vzigpvrG66urohD4HA4aCA56mJ0N0J79rfzt2PKyoO/H77c2tDSQhtocYVK71mn&#10;ajkGv7b9rimmHbAQWYALtZ4rvVfR6stkbUdwcX7C+++/w8cffYuPPvom7737lLOzjWWaJZ0nWe7F&#10;akXuxllXlhwJ9Mr2DYrYSf6sysO1llGMA3ggcYhsc+bleGpHu/8AACAASURBVGCXrC8U1HRmBxm6&#10;x7KJjR44L2XTr3Mcf+8Y3PcAY7GJuoVe0Eyasj2w/+w5+xevYZq1BLjJdeUX1Ho0i+sfXTr++Pu/&#10;95Pjj2oPhKMBP3hsn5yOiwhYlQqDKA4kdM1YwRsLwrRI2mjMLICayufMfJiYDxNpniEE4nrFn/38&#10;5/zzf/Ev+MUnn/Czn/2SP/vzn/OnP/0Zd29e8lvf+S6b1crMXqECm7dPxCOTaEygPPbZo8/fbdJi&#10;9lVRTR4KJfVN0AyAxGCFcsyEH4K1FbYFXzzNt2NCzrg7jQAwczldHr6hbAnai8H6k5RCM/E7YzDW&#10;F0JQs6P53VNKOot1WltBospwiqUfe+psodJdTNPIeSZNGvApqHlziNpGPUphkJmVjEi6g+k5ZX5B&#10;mF8wlDtW5Z5Q9kRmjd8ALa1LJIYVmsbqaFstB4hq9aUUQmkdUb/+4S6FaDMbFFgU03SLPreT2wGh&#10;N09yRgogRQGCWKny1aqwXsPZWeTiYuDqZsXtkxOePTOQcR25ul5zcXnC6cmKECfIB3LW3iji5l5f&#10;a8483Yrkc1ZSnd8a9mlpsS7IPJXa3WS5FEo24WGKSbCiRvM8cdjvmcaReZxYDSvWMTKEQZ+1y+zS&#10;dXoELo6O/v1FDZhHX7L4nuDBqr1AXl5bfI4kIBLJota9LJCkMJFJ+uUKuANiDfdYBFQ3JFFNAzx8&#10;Mo+zCFZtmAYocBbYMgQcfPj4xWIplB/YvSylHWC1WnFxecnV5QUnpyeklLW1QppVILgbE+MFvJ33&#10;OTr6cpp3L5Hlz44GDy0UjbfWTs7+shFW4S4dWKHRS0GMNWGUQgiFzWbg9sk13/rwAz7+9rd4/713&#10;NLtmEKQ6tbQnkBcm9Omq2T0O4JwKC7nVkaZSsilrfn7wlHsHlHFglsAeuJsTr8eJfc5qd+x6U4lI&#10;bTbZNwpsPKTdvHcNft3jAVCSLivk2EDjPqbuO5pqC/lux/avPme+uzdgYaum5Mpneouc+OVoqeYA&#10;8cff/92fwHIxSj86lAKFtqGrKbEckaBuphZRXrrzofmwijFl5XfFAJFqQoMRI00zh8Oe3X5PKXDz&#10;5Jbf//3f5YMPPkBC4N/8m3/Lz3/+CZu18N/+/b/PamVWAKSmAMU4kFOuC8ZTNUtWwe59TnLWehpV&#10;86LUXhZDDDZeUQIfZU8oM6CKO28tjwldrOOkkjgTJS5oFa2s92qIdQzaS8FriegiEAc/HhQmmNbk&#10;AAcrZKI1SjKazZKsfLnNni5wE4xFsBLcQ92Ibv4qlmYbaFanXDzgU5Aw2HNES0VSxqrKbYGkIDJZ&#10;MG+UwmrIhLwnTy9J+8+Y95/C/JyY3jCkLTEdCGjcSjCg5GDCfby5qBusWBopYqMsAUqs4Nbp+7W0&#10;gcrp7BlYvtrc6yKtKXd2vlo0IpKjCisxi4+4Dj0TYiaGwhALq7VwuomcX6y4uTmrAaLvvHPC06dr&#10;bp+subpec3oqxJgQTULGi5xRIJRIUOiFADmPhJDJeUYQkhXoKjlXzbDtdyFnT/8NNSvPmSIuzFPW&#10;/bjbM+9HsLL/0QWjUF0Rb0UVPY2dJ7zl9N4oaqsb6jrsTdu+N9orlKABstCapAWz4ohUpTY39mZj&#10;acBbAYYJSBtD6C08pVQFoLhkdPdnwIo+tbXXzpeuR5DfuKXy9YylPb7yh9PTU25vb7m+vma9WjFO&#10;M9M4V8FJZfxtvF4rKES1lro1tXY8lQ4QOJXF3S2y2D+9NcKnMXj2V/f9XqC74PE9IxTrAqs0CQ7O&#10;xa8HMQpFZiQUrm8u+fDjD/nOb33MB996n9vbK9abwZ5T13Ww4NYKanzNPAA9iyXlU9/WXEeLtk4d&#10;SERaTInyvySBPAzsgFfjyN08M2U6V6oBFZeXIrVJnr+y4/1mdtL1UArHvKuvTP22BIX6e/8I3Rx0&#10;E9fWmSkIUQSZM4fPX7L/9DkyTl3RO4/uc1nHAlxUqnUEHR6r+NW05iV60ktnM01TtRxoD00IzCTc&#10;CJZzNg+WPqFrgEFEGylZHEYQ9f1rJLteuKTCnGZ2h5H9fsftO+/we9//Ib/7g9/lv/v7n/MnP/0p&#10;P/vZz/nedz/k5OyUcdprwE9QsKCtp7fc3DwBYJomcraAOY93M9tOCGpyHIbBP9CnLckWQDbmU6e6&#10;Taj92bQ131QWDpkzZZqZc4E5kOfCsFmxXg2aP5ALEkWzKwbNU5Vx4jAWcuqcHyXYPOWKEKVbaAFF&#10;28kmpxTrc5Jmcs7MJTGL+sw3QeMf8IwVCqv1mhgCU1CL0Yx+r1YELQIhauMou38WLyVumr5kyjxb&#10;z5WgDc3nA9NuhnJPSUKed4zTG6b9C3LaMnAgykwoWu9BmXWkoB1WvaB1kUKSGWTutEABBg2GZE31&#10;skvi1zsKGrTp13/k85K7jw1E+gJADOQ4g8240QBjrsVihDTGoSBB29sPg4Lj8/NMzoGUTknpjGkS&#10;ttvE3d3M7j5zd5/YbWfu7md225nDOFLyRElRNdlVtrTToPFHcYWgvVt04FIBNVicSomUECCFJvwT&#10;SJYqKMOUyHPmbnrJ4X7H9s09N8+ecH51SYwDM81S4ize46sWjNFJqTjnAZkfxHw55Z1hfgVY7O+j&#10;1QSVcxdFWGQJmrhbClIyCWGFsBZ7lWjuHiGg2VgKDlVK1Eh7A/N1lOJCTgGG03mezVmVc3eurxcT&#10;RGazroHvtq56YTGsBobVwMnJCbc3Nzx9+oxf/vIT/vL//Uv224PxBiwuSMcXYjAt2i2YzdraU7dZ&#10;uFgAtrc1jey/LTwG4EsFGm6hrgLTni2aRi945hyIRIrAyemKZ+885cNvfcj733ifi4tzQhTjD55K&#10;L93IAyLtGR5dFz6yo6GWxS8t5dqfV9sZeGi7Pm/GircFYcyZQ1ZrWMMIplQg1RLm94hxYLMWJGjJ&#10;t2nW9g6t2WUDclUmv9US9XBvHT/f4hn9d0cHZkHxmjTpMDHdb8nThOuIHqvS30+XcAO1haK9HWlr&#10;K/74+7/7ky/drGVx2Uc+7hmGUC223UP6t/KjV+qu6ejTp9DL65rWNO1Hpt3I+fqU9995lx9+/wd8&#10;//vf43u//VusTlbqq7QmQCEG/uRPfsof/dE/4rPPP2NYrTi/OGe92RCCMM8z6pMt1sI3anW8qKZ+&#10;R/1iqkAIofUW8YI1HA3d5itQqu/WpKIR0lr2ZkXcUTSDovkDrfrcoIGejgwxSwu4b9tNVcbsqk5E&#10;0yBrNHwhkxet3EvRwdWGcQZEHDyp68YQOtRGWSKaJaD+yLCwSCmwUFATMgSJpsFZN9Jyx5xeMh4+&#10;Zx6/IE0vkfSGgT0rZgYKsWTrjhjRKvaW+VADrLJpeIniNWcBKdFe6jJpWR1fLYgeP8qXvKiCoJos&#10;KsPt2LaPoRtL+77YmuhcMEVBic6pPn2MwnoVONkMnJ5GLi/W3NxuuL3dcHuz5vpqxdVl5OIycHJa&#10;WA0jEvaEkNB0Vi3ZHsKKEAZSypqpVLqxdONyV5PG6xj9Ss981HVAKmpR3O6YplkZYXRftGUBVRI1&#10;Ju1KxjEXeQAu3vJB6T6Qow/7vzRugc7CKi1IuhRLc9Rd43FJCcVVuY7T4wpCBRC9hrmc64p66liq&#10;Vhf6jK026joFfYCV9NeQ7iI+eP0kDpGTk1MuLi84vzhniJGUZ8ZxJKVkgdqWFm+XadklDQz0log+&#10;PdTH2ijt5xxlhPR/87AdwhAHzfLwc50u0lJnPQ5WAgxDZH2y5smzKz786AO+/Z2Pef+D9zi/OFUX&#10;yIOAmEaT5etL0EUls9P32Okr3ZzZGrCKwS2GRGN50hDZB+HNNPFqnNinTCrSrRepfDeXjNdc1NRr&#10;twyF6m5REWF70coVlCOw8bbjbZ/1VPFD9Q4fo/LcQQKSCuPrO7afPye/2ULSNNp66Q4ktQDoYyu+&#10;fy7Ev/Pbv/OTt46Yxnba/w8fpHTnqd5Y9ZXuf0eU3cR7bEdZLhIx/78O3hal9TLJh4nD/Y55mtms&#10;11zfXLHerCuwyFY+dxxH/vE//if80T/63/npn/6cn/3sz/nXf/EJh8OB09NTzs7PiUOs2lxKmfVm&#10;ZYJW752zlrh2c7K2Wu9M7ke0amu+PPaufr/zeSeLyWgBTrrwQowWi+F+fKN/KZrz7YBE3GDbJt1L&#10;guOMTCysT9o8zUULf2ldENs0XgFQNMswRCHEwZhdUDlYxMBVMCGv8ynO/IQaxKRLZKKwA+6gvKbk&#10;1+T0iig7QlFQESUTTYOsKCZEimZV20btWXIzIXtGTQ3AxAqFSfr3AC782o8dR8yseMxL6IBFoRbd&#10;bzvAvq6AydhwAxp2vUAvjBWchJCJQ2ZYZTYnhbMz4fIycnO75umzE5483XB7O3B7u+H88ozT01M2&#10;mw37/R7vRZlT0dowFki9oCkm0Ipa6zxKHqg+W395gSCKcBhH7u/vORwOBAmsVisr5+60L5WZuzw4&#10;1qwe7CN5/IPjADLohaA9SSm0TqwmHIsDDF0zsUi9h2aRaLPAOWglEq/v4pknuoeoipa6CdtAKxBo&#10;o8A1YHcxhOi1Zixoc6G0Lddp6J7wAZ3EOrUWBYoXFxfc3t5wcXlJiIF5npmTAsvBTPnH99BHam6P&#10;lrnRu0EaoOhBiAKK3mXi7vtlBogGeTtYVh5VXR8WqCmG+ERgc7rh6bMnfPDNb/Dt737Etz76gCe3&#10;16zXg7mHc/vOo5R5TMQ9fjRa23MeASasSFqd11r91BSuAkggx8i2FF5MI6+nibEoj9S51TTonLWS&#10;csrF6q8slTV1lXisTrPGZK/My0Ng8TZr0ldRBnQPuTtQRCBpcaw1gXKYuPviBdvPX8Bh1AzAhfq4&#10;vGbgIeU7iEb8w+/97k++bDr6iajbuNvkj5lrCh6t3g3Kg6QK1afvfy++J6p1JzfHmxYXCkhWYkx7&#10;bVe8u7+HkjV1a7NmmubqEsk5s9+PzHPiL//qL/nZz37JL37xS/7sZ5/w8uUX/K2/9bfBrBb13lYn&#10;o/drDatBN7PoQokxtmd7jKqAm82bcVhp5rnhMUTtiOgtzcUjpw0sWOEtL387mOCX4gBlSffWiIxu&#10;bjpEaf+5rzeLmshna6VerLIplqbaGICWgQ4h1oJlOTuStShsyRb7obEogcQQgDKRyz2JN8AbRLZI&#10;2BJkIpAJnm6ZpbrWggChAxR1qXULTlkedC+pJxYV7KFtil8PXNjNypdvH38V53gLzid1vP1bbVMY&#10;SJKCgqEE5vLJWYtuIeYmKqVqEKpEqSslDrBaFU5O4Owicn0VeXJ7wZPbJ9w8ecZmveHlq9eMU9Zi&#10;PwkaS/BA3Qak6nCLZ6UshgtFOqteC1Acx4nddst40D4l69WKYbDAZdQd4ObefiofwxDNJP1QU1tk&#10;SB+Biu4DpS0tPqYGtTnQ6O5YEAv4tFf3fhKNX+oDVJv1QdT1JN0D9bTC3XsujM3XHnrDcT92BySw&#10;1AW70dqzeT2fYCmOm9MTbm5vuL665ORkTUoz03hgmmblO0Hq9T3wuM5BORLHFidULS2PvegrXnT1&#10;KqSBC89Kc/4eg6dvNgFaJBOHwMXlGe+9/x4ff+djPvr2Rzx5esPmdMOwHoxnmUQpVmOBRvaHYMKu&#10;/eC9YyZ9vD/b87BYd3XTNV6km4A5BF7PMy/GA3c5MRdtfpbRsgCpFOZcmEshUWrNFe8pVexB6j+T&#10;DdqX5C3Wl7oU5NHfq3XhaJ8JDVhUnUwXG0OGVRbmO61tcXj9hpDmZnnv6C2FRYfbur8e0A3iH37v&#10;d74UXCwfKHTz06D3McCwkMqm+fjLKPpIimyH98GboVGo5jSP5vaTBDiMB7bbe4YQOD05AUPziObk&#10;P3v6jN/5nR/y27/9XZ4+fUpKiX/9F3/BRx99i3/wD/4B0zQSY1QLRYyLvG2P2QAsdbSBkPpQjxKJ&#10;qnl6yef+VE13kwa2wDIZrR+DC3kxn6S0VE+3YuRO68xOjNAYsoKxRtF6tiHw3m2V0cU/l0wWc2cI&#10;CiZ8/NH7hZgZs6hBOUgiiAGMYIBBJkrZU8qWzB2Fe2CLyIEgs56LpruWLJTs0fUGfix4ttgz6Eaw&#10;RePMtnRLuzL7Xkj7ub8muPgVQYW/ln53n3Gpfy+YWh2Og4vefaLjF2v01vpF2GT5fstNEKnml5GQ&#10;iFGZ9Xp9xsnmnPv7HX/115+x380UaxTna7JupAqE67DwQkPNONUltxaQoJ2QU1GgIUCeEvNhYtzv&#10;meeRQmY9rBhWXnbeLFuhE2wsd8diruThe2+PueiBiGXOmJWi3kcAWtO4YvOmeyJQQqhp26V46qoC&#10;cQ9CrwH23TTWtVfv08bRnlDqc3rn2V6JkW4+VHha9kl3pwUPcStjaXU0YgxsNlob4+zslEBgnkfm&#10;eTRLU6nFw3w0y/F2dLQ1d+wC6a0V/v3jc/x56pOLWhta7IgGBkqE09MTnj17wocff8i3v/ttvvGN&#10;9zm/OKtFEXXJ1w2gc9Br8dLG3R8PxfJj+7Z/9s5a48Cnl00mlJGaJwYSOIjwch55MY3sSlGXSFGL&#10;xVw0kH4uarnw5C5EHoyvBeirQhliH9tHdY/0LsbF0x2DcHtkBwJOAQfJLpoFlatDEcKY2L18zd0X&#10;L0jbnVqTXcm2/50naCZZDzCaVaufCgMXS/zcT8njgSQVVSyIs/iinSZu/yttYL32IJ1rRDe2NQSr&#10;jOGhcBAJjPOkQaDTxP3dHbvtPYOI5RMLw3rFEFfc3Nzw8cff5oc//AE/+tEP+d73vsuPf/xjvvnN&#10;b2r8Qw0yEt68ec0vfvELLi4uWK1WrNfrOrFe6z57xL0/57EcAUOey5H3LKdY749oTCLnoi4SjWiq&#10;E9kKzHRlvGOXT29mttL5IqWgi8Xo7OgYkQomsnHIIppVMqfElFJtqhYdGNlGEBHLKPFYjEyUAmQC&#10;icCMMEOZoWxJ5SVZXiGyAxkRZmX2WZdjFC1+pYIZtVYEy3IpBr4QdZWIpT95qXCVULTy5A54MwR3&#10;hzzGQL7G0XPPR15Fjhezv1rTsIUEapLSJqkHL30cd71BtSvU98XDqkpzYbn1oAjq+hgQWUFZIQT+&#10;8i8/5a//+jmHEYp9poW2dO5cE2y5YFItFr5+6po1IZuBlAuEWK2EpKKdYYE0jry5e8Nh3FdeEoI3&#10;oNI1e7ylv8qf7MdjgWpLUgtKOcvL99GL1LVeC2zZq85JaSIxB5glkMSyaOraczftEXDpxakLwsUz&#10;1dA+oLkVPNjR01ddHajxHt3zLejgY6pxU0bnGNicrLm+vOT25pbrqys2m1WnKLU6Kb2bw+eg9gKR&#10;NifH8RXu/uj/bpkxHtvRuVz82czEvhoim82G2ye3fPztj/jOb32HD775Da5vrs0tTVXycioLPtnL&#10;ox7PLzFD8emsfO94Py/XWweEOmI3JVKaULZ9GGwPb0vm+Xjg9TyzB+YslAQpO7iwztfFgUUb88Ly&#10;3AEAp2NzVX/1vnh4jq2/fu30wKI+T2EgMGRI2wN3n79g9/IVMnnNEB9bB55tfy2Be5OHLScK4h/+&#10;1o9+olpQN0BZ/Hhk4PV+jwKLHjj4d5xwfonuavqzo4K35XazrSP0bA9bUIaVksZXpHlm3O6Y93um&#10;eWZ9ctJ8nGgvkqura957/z2++93v8O677zFNI8NgdTFs8f/Tf/pP+V/+5/+NX/7yz4kx8N5777Fa&#10;tc2pmSRfctgarpaFspz49ohhsUmUV2TNDMlaTjuIVj1ULcf8wEGfe7Vaob5bbVrWQlak+ZRpG6tW&#10;DzSGqum4xueDxVOINWHLM15xUGJUJuzgSkCksBqEKAnKRCkHStlbPYY9hTsyL0G2IDNCNkGlaaJC&#10;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&#10;" - id="image1-7" - x="144.39027" - y="145.30473" /><text - xml:space="preserve" - style="font-size:8.94777px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';fill:#000000;stroke-width:0.760912" - x="59.22551" - y="99.922089" - id="text48"><tspan - sodipodi:role="line" - style="font-weight:bold;font-size:8.94777px;fill:#000000;stroke-width:0.760912" - x="59.22551" - y="99.922089" - id="tspan49">SPQlios API</tspan></text><rect - style="fill:none;stroke:#000000;stroke-width:1.3;stroke-dasharray:2.60000398,2.60000398;stroke-dashoffset:0" - id="rect50" - width="141.88351" - height="45.035332" - x="17.291592" - y="75.534477" - rx="5" - ry="5" /><path - style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1.3;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#RoundedArrow)" - d="m 38.945222,144.01501 c 15.201046,0.99574 24.999562,-2.65083 33.799115,-15.25884" - id="path50" - sodipodi:nodetypes="cc" /><path - style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1.3;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#RoundedArrow)" - d="m 75.967274,146.51446 c 2.925331,-3.24509 6.283122,-11.54669 7.360403,-16.17079" - id="path51" - sodipodi:nodetypes="cc" /><path - style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1.3;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#RoundedArrow)" - d="m 138.52655,144.01501 c -15.20113,0.99574 -24.9997,-2.65083 -33.79931,-15.25884" - id="path53" - sodipodi:nodetypes="cc" /><path - style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1.3;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#RoundedArrow)" - d="m 102.3458,146.51446 c -2.925344,-3.24509 -6.283132,-11.54669 -7.360413,-16.17079" - id="path54" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.5;stroke-dasharray:none;marker-end:url(#ConcaveTriangle)" - d="m 68.985665,40.655103 h 5.984828" - id="path55" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.5;stroke-dasharray:none;marker-end:url(#ConcaveTriangle)" - d="M 77.263825,44.359272 H 71.278997" - id="path56" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.5;stroke-dasharray:none;marker-end:url(#ConcaveTriangle)" - d="m 105.49819,40.655103 h 5.98483" - id="path57" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.5;stroke-dasharray:none;marker-end:url(#ConcaveTriangle)" - d="m 114.30551,44.359272 h -5.98483" - id="path58" - sodipodi:nodetypes="cc" /><text - xml:space="preserve" - style="font-size:3.52778px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';text-align:center;text-anchor:middle;fill:#666666;stroke-width:0.3" - x="91.075272" - y="40.35569" - id="text2"><tspan - sodipodi:role="line" - id="tspan1" - style="font-size:3.52778px;text-align:center;text-anchor:middle;fill:#666666;stroke-width:0.3" - x="91.075272" - y="40.35569">Chimera</tspan><tspan - sodipodi:role="line" - style="font-size:3.52778px;text-align:center;text-anchor:middle;fill:#666666;stroke-width:0.3" - x="91.075272" - y="46.352917" - id="tspan2">SchemeSwitch</tspan></text><text - xml:space="preserve" - style="font-size:3.52778px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';fill:#666666;stroke-width:0.3" - x="150.57195" - y="40.35569" - id="text4"><tspan - sodipodi:role="line" - style="font-size:3.52778px;fill:#666666;stroke-width:0.3" - x="150.57195" - y="40.35569" - id="tspan3">LWE/SIS</tspan><tspan - sodipodi:role="line" - style="font-size:3.52778px;fill:#666666;stroke-width:0.3" - x="150.57195" - y="46.352917" - id="tspan4">Frodo, FrodoPIR</tspan></text><text - xml:space="preserve" - style="font-size:3.52777px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';text-align:center;text-anchor:middle;fill:#ff0000;stroke:#ff0000;stroke-width:1" - x="73.55233" - y="22.175295" - id="text5"><tspan - sodipodi:role="line" - id="tspan5" - style="stroke-width:1" - x="73.55233" - y="22.175295" /></text><text - xml:space="preserve" - style="font-size:8.89214px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';text-align:start;text-anchor:start;fill:#000000;stroke-width:0.756182" - x="65.787498" - y="19.72295" - id="text7"><tspan - sodipodi:role="line" - style="font-size:8.89214px;text-align:start;text-anchor:start;fill:#000000;stroke-width:0.756182" - x="65.787498" - y="19.72295" - id="tspan7">Potential use-cases</tspan></text><text - xml:space="preserve" - style="font-size:3.52778px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';text-align:start;text-anchor:start;fill:#000000;stroke-width:0.3" - x="66.560791" - y="25.474648" - id="text9"><tspan - sodipodi:role="line" - style="font-style:italic;font-size:3.52778px;text-align:start;text-anchor:start;fill:#000000;stroke-width:0.3" - x="66.560791" - y="25.474648" - id="tspan9">that would benefit from the API</tspan></text></g></svg>
D spqlios/lib/docs/logo-sandboxaq-black.svg

@@ -1,139 +0,0 @@

-<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Generator: Adobe Illustrator 24.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> - -<svg - version="1.1" - id="Layer_1" - x="0px" - y="0px" - viewBox="0 0 270 49.4" - style="enable-background:new 0 0 270 49.4;" - xml:space="preserve" - sodipodi:docname="logo-sandboxaq-black.svg" - inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns="http://www.w3.org/2000/svg" - xmlns:svg="http://www.w3.org/2000/svg"><defs - id="defs9839"> - - - - - - - </defs><sodipodi:namedview - id="namedview9837" - pagecolor="#ffffff" - bordercolor="#000000" - borderopacity="0.25" - inkscape:showpageshadow="2" - inkscape:pageopacity="0.0" - inkscape:pagecheckerboard="0" - inkscape:deskcolor="#d1d1d1" - showgrid="false" - inkscape:zoom="1.194332" - inkscape:cx="135.64068" - inkscape:cy="25.118645" - inkscape:window-width="804" - inkscape:window-height="436" - inkscape:window-x="190" - inkscape:window-y="27" - inkscape:window-maximized="0" - inkscape:current-layer="Layer_1" /> -<style - type="text/css" - id="style9786"> - .st0{fill:#EBB028;} - .st1{fill:#FFFFFF;} -</style> -<text - transform="matrix(1 0 0 1 393.832 -491.944)" - class="st1" - style="font-family:'Satoshi-Medium'; font-size:86.2078px;" - id="text9788">SANDBOX </text> -<text - transform="matrix(1 0 0 1 896.332 -491.944)" - class="st1" - style="font-family:'Satoshi-Black'; font-size:86.2078px;" - id="text9790">AQ</text> -<g - id="g9808"> - <g - id="g9800"> - <g - id="g9798"> - <path - class="st0" - d="m 8.9,9.7 v 3.9 l 29.6,17.1 v 2.7 c 0,1.2 -0.6,2.3 -1.6,2.9 L 31,39.8 v -4 L 1.4,18.6 V 15.9 C 1.4,14.7 2,13.6 3.1,13 Z" - id="path9792" /> - <path - class="st0" - d="M 18.3,45.1 3.1,36.3 C 2.1,35.7 1.4,34.6 1.4,33.4 V 26 L 28,41.4 21.5,45.1 c -0.9,0.6 -2.2,0.6 -3.2,0 z" - id="path9794" /> - <path - class="st0" - d="m 21.6,4.3 15.2,8.8 c 1,0.6 1.7,1.7 1.7,2.9 v 7.5 L 11.8,8 18.3,4.3 c 1,-0.6 2.3,-0.6 3.3,0 z" - id="path9796" /> - </g> - </g> - <g - id="g9806"> - <polygon - class="st0" - points="248.1,23.2 248.1,30 251.4,33.8 257.3,33.8 " - id="polygon9802" /> - <path - class="st0" - d="m 246.9,31 -0.1,-0.1 h -0.1 c -0.2,0 -0.4,0 -0.6,0 -3.5,0 -5.7,-2.6 -5.7,-6.7 0,-4.1 2.2,-6.7 5.7,-6.7 3.5,0 5.7,2.6 5.7,6.7 0,0.3 0,0.6 0,0.9 l 3.6,4.2 c 0.7,-1.5 1,-3.2 1,-5.1 0,-6.5 -4.2,-11 -10.3,-11 -6.1,0 -10.3,4.5 -10.3,11 0,6.5 4.2,11 10.3,11 1.2,0 2.3,-0.2 3.4,-0.5 l 0.5,-0.2 z" - id="path9804" /> - </g> - </g><g - id="g9824" - style="fill:#1a1a1a"> - <path - class="st1" - d="m 58.7,13.2 c 4.6,0 7.4,2.5 7.4,6.5 h -4.6 c 0,-1.5 -1.1,-2.4 -2.9,-2.4 -1.9,0 -3.1,0.9 -3.1,2.3 0,1.3 0.7,1.9 2.2,2.2 l 3.2,0.7 c 3.8,0.8 5.6,2.6 5.6,5.9 0,4.1 -3.2,6.8 -8.1,6.8 -4.7,0 -7.8,-2.6 -7.8,-6.5 h 4.6 c 0,1.6 1.1,2.4 3.2,2.4 2.1,0 3.4,-0.8 3.4,-2.2 0,-1.2 -0.5,-1.8 -2,-2.1 l -3.2,-0.7 c -3.8,-0.8 -5.7,-2.9 -5.7,-6.4 0,-3.7 3.2,-6.5 7.8,-6.5 z" - id="path9810" - style="fill:#1a1a1a" /> - <path - class="st1" - d="M 70.4,34.9 78,13.6 h 4.5 l 7.6,21.3 h -4.9 l -1.5,-4.5 h -6.9 l -1.5,4.5 z m 7.7,-8.4 h 4.2 L 80.8,22 c -0.2,-0.7 -0.5,-1.6 -0.6,-2.1 -0.1,0.5 -0.3,1.3 -0.6,2.1 z" - id="path9812" - style="fill:#1a1a1a" /> - <path - class="st1" - d="M 95.3,34.9 V 13.6 h 4.6 l 9,13.5 V 13.6 h 4.6 v 21.3 h -4.6 l -9,-13.5 v 13.5 z" - id="path9814" - style="fill:#1a1a1a" /> - <path - class="st1" - d="M 120.7,34.9 V 13.6 h 8 c 6.2,0 10.6,4.4 10.6,10.7 0,6.2 -4.2,10.6 -10.3,10.6 z m 4.7,-17 v 12.6 h 3.2 c 3.7,0 5.8,-2.3 5.8,-6.3 0,-4 -2.3,-6.4 -6.1,-6.4 h -2.9 z" - id="path9816" - style="fill:#1a1a1a" /> - <path - class="st1" - d="m 145.4,13.6 h 8.8 c 4.3,0 6.9,2.2 6.9,5.9 0,2.3 -1,3.9 -3,4.8 2.1,0.7 3.2,2.3 3.2,4.7 0,3.8 -2.5,5.9 -7.1,5.9 h -8.8 z m 4.7,4.1 v 4.6 h 3.7 c 1.7,0 2.6,-0.8 2.6,-2.4 0,-1.5 -0.9,-2.3 -2.6,-2.3 h -3.7 z m 0,8.5 v 4.6 h 3.9 c 1.7,0 2.6,-0.8 2.6,-2.4 0,-1.4 -0.9,-2.2 -2.6,-2.2 z" - id="path9818" - style="fill:#1a1a1a" /> - <path - class="st1" - d="m 176.5,35.2 c -6.1,0 -10.4,-4.5 -10.4,-11 0,-6.5 4.3,-11 10.4,-11 6.2,0 10.4,4.5 10.4,11 0,6.5 -4.2,11 -10.4,11 z m 0.1,-17.5 c -3.4,0 -5.5,2.4 -5.5,6.5 0,4.1 2.1,6.5 5.5,6.5 3.4,0 5.5,-2.5 5.5,-6.5 0,-4 -2.1,-6.5 -5.5,-6.5 z" - id="path9820" - style="fill:#1a1a1a" /> - <path - class="st1" - d="m 190.4,13.6 h 5.5 l 1.8,2.8 c 0.8,1.2 1.5,2.5 2.5,4.3 l 4.3,-7 h 5.4 l -6.7,10.6 6.7,10.6 h -5.5 L 203,32.7 c -1.1,-1.7 -1.8,-3 -2.8,-4.9 l -4.6,7.1 h -5.5 l 7.1,-10.6 z" - id="path9822" - style="fill:#1a1a1a" /> - </g><path - class="st0" - d="m 229,34.9 h 4.7 L 226,13.6 h -4.3 L 214,34.8 h 4.6 l 1.6,-4.5 h 7.1 z m -5.1,-14.6 c 0,0 0,0 0,0 0,-0.1 0,-0.1 0,0 l 2.2,6.2 h -4.4 z" - id="path9826" /><g - id="g9832"> - <path - class="st1" - d="m 259.5,11.2 h 3.9 v 1 h -1.3 v 3.1 h -1.3 v -3.1 h -1.3 z m 4.5,0 h 1.7 l 0.6,2.5 0.6,-2.5 h 1.7 v 4.1 h -1 v -3.1 l -0.8,3.1 h -0.9 l -0.8,-3.1 v 3.1 h -1 v -4.1 z" - id="path9830" /> - </g> -</svg>
D spqlios/lib/docs/logo-sandboxaq-white.svg

@@ -1,133 +0,0 @@

-<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Generator: Adobe Illustrator 24.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> - -<svg - version="1.1" - id="Layer_1" - x="0px" - y="0px" - viewBox="0 0 270 49.4" - style="enable-background:new 0 0 270 49.4;" - xml:space="preserve" - sodipodi:docname="logo-sandboxaq-white.svg" - inkscape:version="1.2.2 (1:1.2.2+202212051551+b0a8486541)" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns="http://www.w3.org/2000/svg" - xmlns:svg="http://www.w3.org/2000/svg"><defs - id="defs9839" /><sodipodi:namedview - id="namedview9837" - pagecolor="#ffffff" - bordercolor="#000000" - borderopacity="0.25" - inkscape:showpageshadow="2" - inkscape:pageopacity="0.0" - inkscape:pagecheckerboard="0" - inkscape:deskcolor="#d1d1d1" - showgrid="false" - inkscape:zoom="2.3886639" - inkscape:cx="135.22204" - inkscape:cy="25.327967" - inkscape:window-width="1072" - inkscape:window-height="688" - inkscape:window-x="0" - inkscape:window-y="0" - inkscape:window-maximized="1" - inkscape:current-layer="Layer_1" /> -<style - type="text/css" - id="style9786"> - .st0{fill:#EBB028;} - .st1{fill:#FFFFFF;} -</style> -<text - transform="matrix(1 0 0 1 393.832 -491.944)" - class="st1" - style="font-family:'Satoshi-Medium'; font-size:86.2078px;" - id="text9788">SANDBOX </text> -<text - transform="matrix(1 0 0 1 896.332 -491.944)" - class="st1" - style="font-family:'Satoshi-Black'; font-size:86.2078px;" - id="text9790">AQ</text> -<g - id="g9834"> - <g - id="g9828"> - <g - id="g9808"> - <g - id="g9800"> - <g - id="g9798"> - <path - class="st0" - d="M8.9,9.7v3.9l29.6,17.1v2.7c0,1.2-0.6,2.3-1.6,2.9L31,39.8v-4L1.4,18.6v-2.7c0-1.2,0.6-2.3,1.7-2.9 L8.9,9.7z" - id="path9792" /> - <path - class="st0" - d="M18.3,45.1L3.1,36.3c-1-0.6-1.7-1.7-1.7-2.9V26L28,41.4l-6.5,3.7C20.6,45.7,19.3,45.7,18.3,45.1z" - id="path9794" /> - <path - class="st0" - d="M21.6,4.3l15.2,8.8c1,0.6,1.7,1.7,1.7,2.9v7.5L11.8,8l6.5-3.7C19.3,3.7,20.6,3.7,21.6,4.3z" - id="path9796" /> - </g> - </g> - <g - id="g9806"> - <polygon - class="st0" - points="248.1,23.2 248.1,30 251.4,33.8 257.3,33.8 " - id="polygon9802" /> - <path - class="st0" - d="M246.9,31l-0.1-0.1l-0.1,0c-0.2,0-0.4,0-0.6,0c-3.5,0-5.7-2.6-5.7-6.7c0-4.1,2.2-6.7,5.7-6.7 s5.7,2.6,5.7,6.7c0,0.3,0,0.6,0,0.9l3.6,4.2c0.7-1.5,1-3.2,1-5.1c0-6.5-4.2-11-10.3-11c-6.1,0-10.3,4.5-10.3,11s4.2,11,10.3,11 c1.2,0,2.3-0.2,3.4-0.5l0.5-0.2L246.9,31z" - id="path9804" /> - </g> - </g> - <g - id="g9824"> - <path - class="st1" - d="M58.7,13.2c4.6,0,7.4,2.5,7.4,6.5h-4.6c0-1.5-1.1-2.4-2.9-2.4c-1.9,0-3.1,0.9-3.1,2.3c0,1.3,0.7,1.9,2.2,2.2 l3.2,0.7c3.8,0.8,5.6,2.6,5.6,5.9c0,4.1-3.2,6.8-8.1,6.8c-4.7,0-7.8-2.6-7.8-6.5h4.6c0,1.6,1.1,2.4,3.2,2.4 c2.1,0,3.4-0.8,3.4-2.2c0-1.2-0.5-1.8-2-2.1l-3.2-0.7c-3.8-0.8-5.7-2.9-5.7-6.4C50.9,16,54.1,13.2,58.7,13.2z" - id="path9810" /> - <path - class="st1" - d="M70.4,34.9L78,13.6h4.5l7.6,21.3h-4.9l-1.5-4.5h-6.9l-1.5,4.5H70.4z M78.1,26.5h4.2L80.8,22 c-0.2-0.7-0.5-1.6-0.6-2.1c-0.1,0.5-0.3,1.3-0.6,2.1L78.1,26.5z" - id="path9812" /> - <path - class="st1" - d="M95.3,34.9V13.6h4.6l9,13.5V13.6h4.6v21.3h-4.6l-9-13.5v13.5H95.3z" - id="path9814" /> - <path - class="st1" - d="M120.7,34.9V13.6h8c6.2,0,10.6,4.4,10.6,10.7c0,6.2-4.2,10.6-10.3,10.6H120.7z M125.4,17.9v12.6h3.2 c3.7,0,5.8-2.3,5.8-6.3c0-4-2.3-6.4-6.1-6.4H125.4z" - id="path9816" /> - <path - class="st1" - d="M145.4,13.6h8.8c4.3,0,6.9,2.2,6.9,5.9c0,2.3-1,3.9-3,4.8c2.1,0.7,3.2,2.3,3.2,4.7c0,3.8-2.5,5.9-7.1,5.9 h-8.8V13.6z M150.1,17.7v4.6h3.7c1.7,0,2.6-0.8,2.6-2.4c0-1.5-0.9-2.3-2.6-2.3H150.1z M150.1,26.2v4.6h3.9c1.7,0,2.6-0.8,2.6-2.4 c0-1.4-0.9-2.2-2.6-2.2H150.1z" - id="path9818" /> - <path - class="st1" - d="M176.5,35.2c-6.1,0-10.4-4.5-10.4-11s4.3-11,10.4-11c6.2,0,10.4,4.5,10.4,11S182.7,35.2,176.5,35.2z M176.6,17.7c-3.4,0-5.5,2.4-5.5,6.5c0,4.1,2.1,6.5,5.5,6.5c3.4,0,5.5-2.5,5.5-6.5C182.1,20.2,180,17.7,176.6,17.7z" - id="path9820" /> - <path - class="st1" - d="M190.4,13.6h5.5l1.8,2.8c0.8,1.2,1.5,2.5,2.5,4.3l4.3-7h5.4l-6.7,10.6l6.7,10.6h-5.5l-1.4-2.2 c-1.1-1.7-1.8-3-2.8-4.9l-4.6,7.1h-5.5l7.1-10.6L190.4,13.6z" - id="path9822" /> - </g> - <path - class="st0" - d="M229,34.9h4.7L226,13.6h-4.3l-7.7,21.2h4.6l1.6-4.5h7.1L229,34.9z M223.9,20.3 C223.9,20.3,223.9,20.3,223.9,20.3C223.9,20.2,223.9,20.2,223.9,20.3l2.2,6.2h-4.4L223.9,20.3z" - id="path9826" /> - </g> - <g - id="g9832"> - <path - class="st1" - d="M259.5,11.2h3.9v1h-1.3v3.1h-1.3v-3.1h-1.3V11.2L259.5,11.2z M264,11.2h1.7l0.6,2.5l0.6-2.5h1.7v4.1h-1v-3.1 l-0.8,3.1h-0.9l-0.8-3.1v3.1h-1V11.2L264,11.2z" - id="path9830" /> - </g> -</g> -</svg>
D spqlios/lib/manifest.yaml

@@ -1,2 +0,0 @@

-library: spqlios-fft -version: 2.0.0
D spqlios/lib/scripts/auto-release.sh

@@ -1,27 +0,0 @@

-#!/bin/sh - -# this script generates one tag if there is a version change in manifest.yaml -cd `dirname $0`/.. -if [ "v$1" = "v-y" ]; then - echo "production mode!"; -fi -changes=`git diff HEAD~1..HEAD -- manifest.yaml | grep 'version:'` -oldversion=$(echo "$changes" | grep '^-version:' | cut '-d ' -f2) -version=$(echo "$changes" | grep '^+version:' | cut '-d ' -f2) -echo "Versions: $oldversion --> $version" -if [ "v$oldversion" = "v$version" ]; then - echo "Same version - nothing to do"; exit 0; -fi -if [ "v$1" = "v-y" ]; then - git config user.name github-actions - git config user.email github-actions@github.com - git tag -a "v$version" -m "Version $version" - git push origin "v$version" -else -cat <<EOF - # the script would do: - git tag -a "v$version" -m "Version $version" - git push origin "v$version" -EOF -fi -
D spqlios/lib/scripts/ci-pkg

@@ -1,102 +0,0 @@

-#!/bin/sh - -# ONLY USE A PREFIX YOU ARE CONFIDENT YOU CAN WIPE OUT ENTIRELY -CI_INSTALL_PREFIX=/opt/spqlios -CI_REPO_URL=https://spq-dav.algonics.net/ci -WORKDIR=`pwd` -if [ "x$DESTDIR" = "x" ]; then - DESTDIR=/ -else - mkdir -p $DESTDIR - DESTDIR=`realpath $DESTDIR` -fi -DIR=`dirname "$0"` -cd $DIR/.. -DIR=`pwd` - -FULL_UNAME=`uname -a | tr '[A-Z]' '[a-z]'` -HOST=`echo $FULL_UNAME | sed 's/ .*//'` -ARCH=none -case "$HOST" in - *linux*) - DISTRIB=`lsb_release -c | awk '{print $2}' | tr '[A-Z]' '[a-z]'` - HOST=linux-$DISTRIB - ;; - *darwin*) - HOST=darwin - ;; - *mingw*|*msys*) - DISTRIB=`echo $MSYSTEM | tr '[A-Z]' '[a-z]'` - HOST=msys64-$DISTRIB - ;; - *) - echo "Host unknown: $HOST"; - exit 1 -esac -case "$FULL_UNAME" in - *x86_64*) - ARCH=x86_64 - ;; - *aarch64*) - ARCH=aarch64 - ;; - *arm64*) - ARCH=arm64 - ;; - *) - echo "Architecture unknown: $FULL_UNAME"; - exit 1 -esac -UNAME="$HOST-$ARCH" -CMH= -if [ -d lib/spqlios/.git ]; then - CMH=`git submodule status | sed 's/\(..........\).*/\1/'` -else - CMH=`git rev-parse HEAD | sed 's/\(..........\).*/\1/'` -fi -FNAME=spqlios-arithmetic-$CMH-$UNAME.tar.gz - -cat <<EOF -================= CI MINI-PACKAGER ================== -Work Dir: WORKDIR=$WORKDIR -Spq Dir: DIR=$DIR -Install Root: DESTDIR=$DESTDIR -Install Prefix: CI_INSTALL_PREFIX=$CI_INSTALL_PREFIX -Archive Name: FNAME=$FNAME -CI WebDav: CI_REPO_URL=$CI_REPO_URL -===================================================== -EOF - -if [ "x$1" = "xcreate" ]; then - rm -rf dist - cmake -B build -S . -DCMAKE_INSTALL_PREFIX="$CI_INSTALL_PREFIX" -DCMAKE_BUILD_TYPE=Release -DENABLE_TESTING=ON -DWARNING_PARANOID=ON -DDEVMODE_INSTALL=ON || exit 1 - cmake --build build || exit 1 - rm -rf "$DIR/dist" 2>/dev/null - rm -f "$DIR/$FNAME" 2>/dev/null - DESTDIR="$DIR/dist" cmake --install build || exit 1 - if [ -d "$DIR/dist$CI_INSTALL_PREFIX" ]; then - tar -C "$DIR/dist" -cvzf "$DIR/$FNAME" . - else - # fix since msys can mess up the paths - REAL_DEST=`find "$DIR/dist" -type d -exec test -d "{}$CI_INSTALL_PREFIX" \; -print` - echo "REAL_DEST: $REAL_DEST" - [ -d "$REAL_DEST$CI_INSTALL_PREFIX" ] && tar -C "$REAL_DEST" -cvzf "$DIR/$FNAME" . - fi - [ -f "$DIR/$FNAME" ] || { echo "failed to create $DIR/$FNAME"; exit 1; } - [ "x$CI_CREDS" = "x" ] && { echo "CI_CREDS is not set: not uploading"; exit 1; } - curl -u "$CI_CREDS" -T "$DIR/$FNAME" "$CI_REPO_URL/$FNAME" -fi - -if [ "x$1" = "xinstall" ]; then - [ "x$CI_CREDS" = "x" ] && { echo "CI_CREDS is not set: not downloading"; exit 1; } - # cleaning - rm -rf "$DESTDIR$CI_INSTALL_PREFIX"/* 2>/dev/null - rm -f "$DIR/$FNAME" 2>/dev/null - # downloading - curl -u "$CI_CREDS" -o "$DIR/$FNAME" "$CI_REPO_URL/$FNAME" - [ -f "$DIR/$FNAME" ] || { echo "failed to download $DIR/$FNAME"; exit 0; } - # installing - mkdir -p $DESTDIR - tar -C "$DESTDIR" -xvzf "$DIR/$FNAME" - exit 0 -fi
D spqlios/lib/scripts/prepare-release

@@ -1,181 +0,0 @@

-#!/usr/bin/perl -## -## This script will help update manifest.yaml and Changelog.md before a release -## Any merge to master that changes the version line in manifest.yaml -## is considered as a new release. -## -## When ready to make a release, please run ./scripts/prepare-release -## and commit push the final result! -use File::Basename; -use Cwd 'abs_path'; - -# find its way to the root of git's repository -my $scriptsdirname = dirname(abs_path(__FILE__)); -chdir "$scriptsdirname/.."; -print "✓ Entering directory:".`pwd`; - -# ensures that the current branch is ahead of origin/main -my $diff= `git diff`; -chop $diff; -if ($diff =~ /./) { - die("ERROR: Please commit all the changes before calling the prepare-release script."); -} else { - print("✓ All changes are comitted.\n"); -} -system("git fetch origin"); -my $vcount = `git rev-list --left-right --count origin/main...HEAD`; -$vcount =~ /^([0-9]+)[ \t]*([0-9]+)$/; -if ($2>0) { - die("ERROR: the current HEAD is not ahead of origin/main\n. Please use git merge origin/main."); -} else { - print("✓ Current HEAD is up to date with origin/main.\n"); -} - -mkdir ".changes"; -my $currentbranch = `git rev-parse --abbrev-ref HEAD`; -chop $currentbranch; -$currentbranch =~ s/[^a-zA-Z._-]+/-/g; -my $changefile=".changes/$currentbranch.md"; -my $origmanifestfile=".changes/$currentbranch--manifest.yaml"; -my $origchangelogfile=".changes/$currentbranch--Changelog.md"; - -my $exit_code=system("wget -O $origmanifestfile https://raw.githubusercontent.com/tfhe/spqlios-fft/main/manifest.yaml"); -if ($exit_code!=0 or ! -f $origmanifestfile) { - die("ERROR: failed to download manifest.yaml"); -} -$exit_code=system("wget -O $origchangelogfile https://raw.githubusercontent.com/tfhe/spqlios-fft/main/Changelog.md"); -if ($exit_code!=0 or ! -f $origchangelogfile) { - die("ERROR: failed to download Changelog.md"); -} - -# read the current version (from origin/main manifest) -my $vmajor = 0; -my $vminor = 0; -my $vpatch = 0; -my $versionline = `grep '^version: ' $origmanifestfile | cut -d" " -f2`; -chop $versionline; -if (not $versionline =~ /^([0-9]+)\.([0-9]+)\.([0-9]+)$/) { - die("ERROR: invalid version in manifest file: $versionline\n"); -} else { - $vmajor = int($1); - $vminor = int($2); - $vpatch = int($3); -} -print "Version in manifest file: $vmajor.$vminor.$vpatch\n"; - -if (not -f $changefile) { - ## create a changes file - open F,">$changefile"; - print F "# Changefile for branch $currentbranch\n\n"; - print F "## Type of release (major,minor,patch)?\n\n"; - print F "releasetype: patch\n\n"; - print F "## What has changed (please edit)?\n\n"; - print F "- This has changed.\n"; - close F; -} - -system("editor $changefile"); - -# compute the new version -my $nvmajor; -my $nvminor; -my $nvpatch; -my $changelog; -my $recordchangelog=0; -open F,"$changefile"; -while ($line=<F>) { - chop $line; - if ($recordchangelog) { - ($line =~ /^$/) and next; - $changelog .= "$line\n"; - next; - } - if ($line =~ /^releasetype *: *patch *$/) { - $nvmajor=$vmajor; - $nvminor=$vminor; - $nvpatch=$vpatch+1; - } - if ($line =~ /^releasetype *: *minor *$/) { - $nvmajor=$vmajor; - $nvminor=$vminor+1; - $nvpatch=0; - } - if ($line =~ /^releasetype *: *major *$/) { - $nvmajor=$vmajor+1; - $nvminor=0; - $nvpatch=0; - } - if ($line =~ /^## What has changed/) { - $recordchangelog=1; - } -} -close F; -print "New version: $nvmajor.$nvminor.$nvpatch\n"; -print "Changes:\n$changelog"; - -# updating manifest.yaml -open F,"manifest.yaml"; -open G,">.changes/manifest.yaml"; -while ($line=<F>) { - if ($line =~ /^version *: */) { - print G "version: $nvmajor.$nvminor.$nvpatch\n"; - next; - } - print G $line; -} -close F; -close G; -# updating Changelog.md -open F,"$origchangelogfile"; -open G,">.changes/Changelog.md"; -print G <<EOF -# Changelog - -All notable changes to this project will be documented in this file. -this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -EOF -; -print G "## [$nvmajor.$nvminor.$nvpatch] - ".`date '+%Y-%m-%d'`."\n"; -print G "$changelog\n"; -my $skip_section=1; -while ($line=<F>) { - if ($line =~ /^## +\[([0-9]+)\.([0-9]+)\.([0-9]+)\] +/) { - if ($1>$nvmajor) { - die("ERROR: found larger version $1.$2.$3 in the Changelog.md\n"); - } elsif ($1<$nvmajor) { - $skip_section=0; - } elsif ($2>$nvminor) { - die("ERROR: found larger version $1.$2.$3 in the Changelog.md\n"); - } elsif ($2<$nvminor) { - $skip_section=0; - } elsif ($3>$nvpatch) { - die("ERROR: found larger version $1.$2.$3 in the Changelog.md\n"); - } elsif ($2<$nvpatch) { - $skip_section=0; - } else { - $skip_section=1; - } - } - ($skip_section) and next; - print G $line; -} -close F; -close G; - -print "-------------------------------------\n"; -print "THIS WILL BE UPDATED:\n"; -print "-------------------------------------\n"; -system("diff -u manifest.yaml .changes/manifest.yaml"); -system("diff -u Changelog.md .changes/Changelog.md"); -print "-------------------------------------\n"; -print "To proceed: press <enter> otherwise <CTRL+C>\n"; -my $bla; -$bla=<STDIN>; -system("cp -vf .changes/manifest.yaml manifest.yaml"); -system("cp -vf .changes/Changelog.md Changelog.md"); -system("git commit -a -m \"Update version and changelog.\""); -system("git push"); -print("✓ Changes have been committed and pushed!\n"); -print("✓ A new release will be created when this branch is merged to main.\n"); -
D spqlios/lib/spqlios/CMakeLists.txt

@@ -1,223 +0,0 @@

-enable_language(ASM) - -# C source files that are compiled for all targets (i.e. reference code) -set(SRCS_GENERIC - commons.c - commons_private.c - coeffs/coeffs_arithmetic.c - arithmetic/vec_znx.c - arithmetic/vec_znx_dft.c - arithmetic/vector_matrix_product.c - cplx/cplx_common.c - cplx/cplx_conversions.c - cplx/cplx_fft_asserts.c - cplx/cplx_fft_ref.c - cplx/cplx_fftvec_ref.c - cplx/cplx_ifft_ref.c - cplx/spqlios_cplx_fft.c - reim4/reim4_arithmetic_ref.c - reim4/reim4_fftvec_addmul_ref.c - reim4/reim4_fftvec_conv_ref.c - reim/reim_conversions.c - reim/reim_fft_ifft.c - reim/reim_fft_ref.c - reim/reim_fftvec_addmul_ref.c - reim/reim_ifft_ref.c - reim/reim_ifft_ref.c - reim/reim_to_tnx_ref.c - q120/q120_ntt.c - q120/q120_arithmetic_ref.c - q120/q120_arithmetic_simple.c - arithmetic/scalar_vector_product.c - arithmetic/vec_znx_big.c - arithmetic/znx_small.c - arithmetic/module_api.c - arithmetic/zn_vmp_int8_ref.c - arithmetic/zn_vmp_int16_ref.c - arithmetic/zn_vmp_int32_ref.c - arithmetic/zn_vmp_ref.c - arithmetic/zn_api.c - arithmetic/zn_conversions_ref.c - arithmetic/zn_approxdecomp_ref.c - arithmetic/vec_rnx_api.c - arithmetic/vec_rnx_conversions_ref.c - arithmetic/vec_rnx_svp_ref.c - reim/reim_execute.c - cplx/cplx_execute.c - reim4/reim4_execute.c - arithmetic/vec_rnx_arithmetic.c - arithmetic/vec_rnx_approxdecomp_ref.c - arithmetic/vec_rnx_vmp_ref.c -) -# C or assembly source files compiled only on x86 targets -set(SRCS_X86 - ) -# C or assembly source files compiled only on aarch64 targets -set(SRCS_AARCH64 - cplx/cplx_fallbacks_aarch64.c - reim/reim_fallbacks_aarch64.c - reim4/reim4_fallbacks_aarch64.c - q120/q120_fallbacks_aarch64.c - reim/reim_fft_neon.c -) - -# C or assembly source files compiled only on x86: avx, avx2, fma targets -set(SRCS_FMA_C - arithmetic/vector_matrix_product_avx.c - cplx/cplx_conversions_avx2_fma.c - cplx/cplx_fft_avx2_fma.c - cplx/cplx_fft_sse.c - cplx/cplx_fftvec_avx2_fma.c - cplx/cplx_ifft_avx2_fma.c - reim4/reim4_arithmetic_avx2.c - reim4/reim4_fftvec_conv_fma.c - reim4/reim4_fftvec_addmul_fma.c - reim/reim_conversions_avx.c - reim/reim_fft4_avx_fma.c - reim/reim_fft8_avx_fma.c - reim/reim_ifft4_avx_fma.c - reim/reim_ifft8_avx_fma.c - reim/reim_fft_avx2.c - reim/reim_ifft_avx2.c - reim/reim_to_tnx_avx.c - reim/reim_fftvec_addmul_fma.c -) -set(SRCS_FMA_ASM - cplx/cplx_fft16_avx_fma.s - cplx/cplx_ifft16_avx_fma.s - reim/reim_fft16_avx_fma.s - reim/reim_ifft16_avx_fma.s -) -set(SRCS_FMA_WIN32_ASM - cplx/cplx_fft16_avx_fma_win32.s - cplx/cplx_ifft16_avx_fma_win32.s - reim/reim_fft16_avx_fma_win32.s - reim/reim_ifft16_avx_fma_win32.s -) -set_source_files_properties(${SRCS_FMA_C} PROPERTIES COMPILE_OPTIONS "-mfma;-mavx;-mavx2") -set_source_files_properties(${SRCS_FMA_ASM} PROPERTIES COMPILE_OPTIONS "-mfma;-mavx;-mavx2") - -# C or assembly source files compiled only on x86: avx512f/vl/dq + fma targets -set(SRCS_AVX512 - cplx/cplx_fft_avx512.c - ) -set_source_files_properties(${SRCS_AVX512} PROPERTIES COMPILE_OPTIONS "-mfma;-mavx512f;-mavx512vl;-mavx512dq") - -# C or assembly source files compiled only on x86: avx2 + bmi targets -set(SRCS_AVX2 - arithmetic/vec_znx_avx.c - coeffs/coeffs_arithmetic_avx.c - arithmetic/vec_znx_dft_avx2.c - arithmetic/zn_vmp_int8_avx.c - arithmetic/zn_vmp_int16_avx.c - arithmetic/zn_vmp_int32_avx.c - q120/q120_arithmetic_avx2.c - q120/q120_ntt_avx2.c - arithmetic/vec_rnx_arithmetic_avx.c - arithmetic/vec_rnx_approxdecomp_avx.c - arithmetic/vec_rnx_vmp_avx.c - -) -set_source_files_properties(${SRCS_AVX2} PROPERTIES COMPILE_OPTIONS "-mbmi2;-mavx2") - -# C source files on float128 via libquadmath on x86 targets targets -set(SRCS_F128 - cplx_f128/cplx_fft_f128.c - cplx_f128/cplx_fft_f128.h - ) - -# H header files containing the public API (these headers are installed) -set(HEADERSPUBLIC - commons.h - arithmetic/vec_znx_arithmetic.h - arithmetic/vec_rnx_arithmetic.h - arithmetic/zn_arithmetic.h - cplx/cplx_fft.h - reim/reim_fft.h - q120/q120_common.h - q120/q120_arithmetic.h - q120/q120_ntt.h - ) - -# H header files containing the private API (these headers are used internally) -set(HEADERSPRIVATE - commons_private.h - cplx/cplx_fft_internal.h - cplx/cplx_fft_private.h - reim4/reim4_arithmetic.h - reim4/reim4_fftvec_internal.h - reim4/reim4_fftvec_private.h - reim4/reim4_fftvec_public.h - reim/reim_fft_internal.h - reim/reim_fft_private.h - q120/q120_arithmetic_private.h - q120/q120_ntt_private.h - arithmetic/vec_znx_arithmetic.h - arithmetic/vec_rnx_arithmetic_private.h - arithmetic/vec_rnx_arithmetic_plugin.h - arithmetic/zn_arithmetic_private.h - arithmetic/zn_arithmetic_plugin.h - coeffs/coeffs_arithmetic.h - reim/reim_fft_core_template.h -) - -set(SPQLIOSSOURCES - ${SRCS_GENERIC} - ${HEADERSPUBLIC} - ${HEADERSPRIVATE} - ) -if (${X86}) - set(SPQLIOSSOURCES ${SPQLIOSSOURCES} - ${SRCS_X86} - ${SRCS_FMA_C} - ${SRCS_FMA_ASM} - ${SRCS_AVX2} - ${SRCS_AVX512} - ) -elseif (${X86_WIN32}) - set(SPQLIOSSOURCES ${SPQLIOSSOURCES} - #${SRCS_X86} - ${SRCS_FMA_C} - ${SRCS_FMA_WIN32_ASM} - ${SRCS_AVX2} - ${SRCS_AVX512} - ) -elseif (${AARCH64}) - set(SPQLIOSSOURCES ${SPQLIOSSOURCES} - ${SRCS_AARCH64} - ) -endif () - - -set(SPQLIOSLIBDEP - m # libmath depencency for cosinus/sinus functions - ) - -if (ENABLE_SPQLIOS_F128) - find_library(quadmath REQUIRED NAMES quadmath) - set(SPQLIOSSOURCES ${SPQLIOSSOURCES} ${SRCS_F128}) - set(SPQLIOSLIBDEP ${SPQLIOSLIBDEP} quadmath) -endif (ENABLE_SPQLIOS_F128) - -add_library(libspqlios-static STATIC ${SPQLIOSSOURCES}) -add_library(libspqlios SHARED ${SPQLIOSSOURCES}) -set_property(TARGET libspqlios-static PROPERTY POSITION_INDEPENDENT_CODE ON) -set_property(TARGET libspqlios PROPERTY OUTPUT_NAME spqlios) -set_property(TARGET libspqlios-static PROPERTY OUTPUT_NAME spqlios) -set_property(TARGET libspqlios PROPERTY POSITION_INDEPENDENT_CODE ON) -set_property(TARGET libspqlios PROPERTY SOVERSION ${SPQLIOS_VERSION_MAJOR}) -set_property(TARGET libspqlios PROPERTY VERSION ${SPQLIOS_VERSION}) -if (NOT APPLE) -target_link_options(libspqlios-static PUBLIC -Wl,--no-undefined) -target_link_options(libspqlios PUBLIC -Wl,--no-undefined) -endif() -target_link_libraries(libspqlios ${SPQLIOSLIBDEP}) -target_link_libraries(libspqlios-static ${SPQLIOSLIBDEP}) -install(TARGETS libspqlios-static) -install(TARGETS libspqlios) - -# install the public headers only -foreach (file ${HEADERSPUBLIC}) - get_filename_component(dir ${file} DIRECTORY) - install(FILES ${file} DESTINATION include/spqlios/${dir}) -endforeach ()
D spqlios/lib/spqlios/arithmetic/module_api.c

@@ -1,164 +0,0 @@

-#include <string.h> - -#include "vec_znx_arithmetic_private.h" - -static void fill_generic_virtual_table(MODULE* module) { - // TODO add default ref handler here - module->func.vec_znx_zero = vec_znx_zero_ref; - module->func.vec_znx_copy = vec_znx_copy_ref; - module->func.vec_znx_negate = vec_znx_negate_ref; - module->func.vec_znx_add = vec_znx_add_ref; - module->func.vec_znx_sub = vec_znx_sub_ref; - module->func.vec_znx_rotate = vec_znx_rotate_ref; - module->func.vec_znx_automorphism = vec_znx_automorphism_ref; - module->func.vec_znx_normalize_base2k = vec_znx_normalize_base2k_ref; - module->func.vec_znx_normalize_base2k_tmp_bytes = vec_znx_normalize_base2k_tmp_bytes_ref; - if (CPU_SUPPORTS("avx2")) { - // TODO add avx handlers here - module->func.vec_znx_negate = vec_znx_negate_avx; - module->func.vec_znx_add = vec_znx_add_avx; - module->func.vec_znx_sub = vec_znx_sub_avx; - } -} - -static void fill_fft64_virtual_table(MODULE* module) { - // TODO add default ref handler here - // module->func.vec_znx_dft = ...; - module->func.vec_znx_big_normalize_base2k = fft64_vec_znx_big_normalize_base2k; - module->func.vec_znx_big_normalize_base2k_tmp_bytes = fft64_vec_znx_big_normalize_base2k_tmp_bytes; - module->func.vec_znx_big_range_normalize_base2k = fft64_vec_znx_big_range_normalize_base2k; - module->func.vec_znx_big_range_normalize_base2k_tmp_bytes = fft64_vec_znx_big_range_normalize_base2k_tmp_bytes; - module->func.vec_znx_dft = fft64_vec_znx_dft; - module->func.vec_znx_idft = fft64_vec_znx_idft; - module->func.vec_znx_idft_tmp_bytes = fft64_vec_znx_idft_tmp_bytes; - module->func.vec_znx_idft_tmp_a = fft64_vec_znx_idft_tmp_a; - module->func.vec_znx_big_add = fft64_vec_znx_big_add; - module->func.vec_znx_big_add_small = fft64_vec_znx_big_add_small; - module->func.vec_znx_big_add_small2 = fft64_vec_znx_big_add_small2; - module->func.vec_znx_big_sub = fft64_vec_znx_big_sub; - module->func.vec_znx_big_sub_small_a = fft64_vec_znx_big_sub_small_a; - module->func.vec_znx_big_sub_small_b = fft64_vec_znx_big_sub_small_b; - module->func.vec_znx_big_sub_small2 = fft64_vec_znx_big_sub_small2; - module->func.vec_znx_big_rotate = fft64_vec_znx_big_rotate; - module->func.vec_znx_big_automorphism = fft64_vec_znx_big_automorphism; - module->func.svp_prepare = fft64_svp_prepare_ref; - module->func.svp_apply_dft = fft64_svp_apply_dft_ref; - module->func.znx_small_single_product = fft64_znx_small_single_product; - module->func.znx_small_single_product_tmp_bytes = fft64_znx_small_single_product_tmp_bytes; - module->func.vmp_prepare_contiguous = fft64_vmp_prepare_contiguous_ref; - module->func.vmp_prepare_contiguous_tmp_bytes = fft64_vmp_prepare_contiguous_tmp_bytes; - module->func.vmp_apply_dft = fft64_vmp_apply_dft_ref; - module->func.vmp_apply_dft_tmp_bytes = fft64_vmp_apply_dft_tmp_bytes; - module->func.vmp_apply_dft_to_dft = fft64_vmp_apply_dft_to_dft_ref; - module->func.vmp_apply_dft_to_dft_tmp_bytes = fft64_vmp_apply_dft_to_dft_tmp_bytes; - module->func.bytes_of_vec_znx_dft = fft64_bytes_of_vec_znx_dft; - module->func.bytes_of_vec_znx_dft = fft64_bytes_of_vec_znx_dft; - module->func.bytes_of_vec_znx_dft = fft64_bytes_of_vec_znx_dft; - module->func.bytes_of_vec_znx_big = fft64_bytes_of_vec_znx_big; - module->func.bytes_of_svp_ppol = fft64_bytes_of_svp_ppol; - module->func.bytes_of_vmp_pmat = fft64_bytes_of_vmp_pmat; - if (CPU_SUPPORTS("avx2")) { - // TODO add avx handlers here - // TODO: enable when avx implementation is done - module->func.vmp_prepare_contiguous = fft64_vmp_prepare_contiguous_avx; - module->func.vmp_apply_dft = fft64_vmp_apply_dft_avx; - module->func.vmp_apply_dft_to_dft = fft64_vmp_apply_dft_to_dft_avx; - } -} - -static void fill_ntt120_virtual_table(MODULE* module) { - // TODO add default ref handler here - // module->func.vec_znx_dft = ...; - if (CPU_SUPPORTS("avx2")) { - // TODO add avx handlers here - module->func.vec_znx_dft = ntt120_vec_znx_dft_avx; - module->func.vec_znx_idft = ntt120_vec_znx_idft_avx; - module->func.vec_znx_idft_tmp_bytes = ntt120_vec_znx_idft_tmp_bytes_avx; - module->func.vec_znx_idft_tmp_a = ntt120_vec_znx_idft_tmp_a_avx; - } -} - -static void fill_virtual_table(MODULE* module) { - fill_generic_virtual_table(module); - switch (module->module_type) { - case FFT64: - fill_fft64_virtual_table(module); - break; - case NTT120: - fill_ntt120_virtual_table(module); - break; - default: - NOT_SUPPORTED(); // invalid type - } -} - -static void fill_fft64_precomp(MODULE* module) { - // fill any necessary precomp stuff - module->mod.fft64.p_conv = new_reim_from_znx64_precomp(module->m, 50); - module->mod.fft64.p_fft = new_reim_fft_precomp(module->m, 0); - module->mod.fft64.p_reim_to_znx = new_reim_to_znx64_precomp(module->m, module->m, 63); - module->mod.fft64.p_ifft = new_reim_ifft_precomp(module->m, 0); - module->mod.fft64.p_addmul = new_reim_fftvec_addmul_precomp(module->m); - module->mod.fft64.mul_fft = new_reim_fftvec_mul_precomp(module->m); -} -static void fill_ntt120_precomp(MODULE* module) { - // fill any necessary precomp stuff - if (CPU_SUPPORTS("avx2")) { - module->mod.q120.p_ntt = q120_new_ntt_bb_precomp(module->nn); - module->mod.q120.p_intt = q120_new_intt_bb_precomp(module->nn); - } -} - -static void fill_module_precomp(MODULE* module) { - switch (module->module_type) { - case FFT64: - fill_fft64_precomp(module); - break; - case NTT120: - fill_ntt120_precomp(module); - break; - default: - NOT_SUPPORTED(); // invalid type - } -} - -static void fill_module(MODULE* module, uint64_t nn, MODULE_TYPE mtype) { - // init to zero to ensure that any non-initialized field bug is detected - // by at least a "proper" segfault - memset(module, 0, sizeof(MODULE)); - module->module_type = mtype; - module->nn = nn; - module->m = nn >> 1; - fill_module_precomp(module); - fill_virtual_table(module); -} - -EXPORT MODULE* new_module_info(uint64_t N, MODULE_TYPE mtype) { - MODULE* m = (MODULE*)malloc(sizeof(MODULE)); - fill_module(m, N, mtype); - return m; -} - -EXPORT void delete_module_info(MODULE* mod) { - switch (mod->module_type) { - case FFT64: - free(mod->mod.fft64.p_conv); - free(mod->mod.fft64.p_fft); - free(mod->mod.fft64.p_ifft); - free(mod->mod.fft64.p_reim_to_znx); - free(mod->mod.fft64.mul_fft); - free(mod->mod.fft64.p_addmul); - break; - case NTT120: - if (CPU_SUPPORTS("avx2")) { - q120_del_ntt_bb_precomp(mod->mod.q120.p_ntt); - q120_del_intt_bb_precomp(mod->mod.q120.p_intt); - } - break; - default: - break; - } - free(mod); -} - -EXPORT uint64_t module_get_n(const MODULE* module) { return module->nn; }
D spqlios/lib/spqlios/arithmetic/scalar_vector_product.c

@@ -1,63 +0,0 @@

-#include <string.h> - -#include "vec_znx_arithmetic_private.h" - -EXPORT uint64_t bytes_of_svp_ppol(const MODULE* module) { return module->func.bytes_of_svp_ppol(module); } - -EXPORT uint64_t fft64_bytes_of_svp_ppol(const MODULE* module) { return module->nn * sizeof(double); } - -EXPORT SVP_PPOL* new_svp_ppol(const MODULE* module) { return spqlios_alloc(bytes_of_svp_ppol(module)); } - -EXPORT void delete_svp_ppol(SVP_PPOL* ppol) { spqlios_free(ppol); } - -// public wrappers -EXPORT void svp_prepare(const MODULE* module, // N - SVP_PPOL* ppol, // output - const int64_t* pol // a -) { - module->func.svp_prepare(module, ppol, pol); -} - -/** @brief prepares a svp polynomial */ -EXPORT void fft64_svp_prepare_ref(const MODULE* module, // N - SVP_PPOL* ppol, // output - const int64_t* pol // a -) { - reim_from_znx64(module->mod.fft64.p_conv, ppol, pol); - reim_fft(module->mod.fft64.p_fft, (double*)ppol); -} - -EXPORT void svp_apply_dft(const MODULE* module, // N - const VEC_ZNX_DFT* res, uint64_t res_size, // output - const SVP_PPOL* ppol, // prepared pol - const int64_t* a, uint64_t a_size, uint64_t a_sl) { - module->func.svp_apply_dft(module, // N - res, - res_size, // output - ppol, // prepared pol - a, a_size, a_sl); -} - -// result = ppol * a -EXPORT void fft64_svp_apply_dft_ref(const MODULE* module, // N - const VEC_ZNX_DFT* res, uint64_t res_size, // output - const SVP_PPOL* ppol, // prepared pol - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->nn; - double* const dres = (double*)res; - double* const dppol = (double*)ppol; - - const uint64_t auto_end_idx = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < auto_end_idx; ++i) { - const int64_t* a_ptr = a + i * a_sl; - double* const res_ptr = dres + i * nn; - // copy the polynomial to res, apply fft in place, call fftvec_mul in place. - reim_from_znx64(module->mod.fft64.p_conv, res_ptr, a_ptr); - reim_fft(module->mod.fft64.p_fft, res_ptr); - reim_fftvec_mul(module->mod.fft64.mul_fft, res_ptr, res_ptr, dppol); - } - - // then extend with zeros - memset(dres + auto_end_idx * nn, 0, (res_size - auto_end_idx) * nn * sizeof(double)); -}
D spqlios/lib/spqlios/arithmetic/vec_rnx_api.c

@@ -1,318 +0,0 @@

-#include <string.h> - -#include "vec_rnx_arithmetic_private.h" - -void fft64_init_rnx_module_precomp(MOD_RNX* module) { - // Add here initialization of items that are in the precomp - const uint64_t m = module->m; - module->precomp.fft64.p_fft = new_reim_fft_precomp(m, 0); - module->precomp.fft64.p_ifft = new_reim_ifft_precomp(m, 0); - module->precomp.fft64.p_fftvec_mul = new_reim_fftvec_mul_precomp(m); - module->precomp.fft64.p_fftvec_addmul = new_reim_fftvec_addmul_precomp(m); -} - -void fft64_finalize_rnx_module_precomp(MOD_RNX* module) { - // Add here deleters for items that are in the precomp - delete_reim_fft_precomp(module->precomp.fft64.p_fft); - delete_reim_ifft_precomp(module->precomp.fft64.p_ifft); - delete_reim_fftvec_mul_precomp(module->precomp.fft64.p_fftvec_mul); - delete_reim_fftvec_addmul_precomp(module->precomp.fft64.p_fftvec_addmul); -} - -void fft64_init_rnx_module_vtable(MOD_RNX* module) { - // Add function pointers here - module->vtable.vec_rnx_add = vec_rnx_add_ref; - module->vtable.vec_rnx_zero = vec_rnx_zero_ref; - module->vtable.vec_rnx_copy = vec_rnx_copy_ref; - module->vtable.vec_rnx_negate = vec_rnx_negate_ref; - module->vtable.vec_rnx_sub = vec_rnx_sub_ref; - module->vtable.vec_rnx_rotate = vec_rnx_rotate_ref; - module->vtable.vec_rnx_automorphism = vec_rnx_automorphism_ref; - module->vtable.vec_rnx_mul_xp_minus_one = vec_rnx_mul_xp_minus_one_ref; - module->vtable.rnx_vmp_apply_dft_to_dft_tmp_bytes = fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_ref; - module->vtable.rnx_vmp_apply_dft_to_dft = fft64_rnx_vmp_apply_dft_to_dft_ref; - module->vtable.rnx_vmp_apply_tmp_a_tmp_bytes = fft64_rnx_vmp_apply_tmp_a_tmp_bytes_ref; - module->vtable.rnx_vmp_apply_tmp_a = fft64_rnx_vmp_apply_tmp_a_ref; - module->vtable.rnx_vmp_prepare_contiguous_tmp_bytes = fft64_rnx_vmp_prepare_contiguous_tmp_bytes_ref; - module->vtable.rnx_vmp_prepare_contiguous = fft64_rnx_vmp_prepare_contiguous_ref; - module->vtable.bytes_of_rnx_vmp_pmat = fft64_bytes_of_rnx_vmp_pmat; - module->vtable.rnx_approxdecomp_from_tnxdbl = rnx_approxdecomp_from_tnxdbl_ref; - module->vtable.vec_rnx_to_znx32 = vec_rnx_to_znx32_ref; - module->vtable.vec_rnx_from_znx32 = vec_rnx_from_znx32_ref; - module->vtable.vec_rnx_to_tnx32 = vec_rnx_to_tnx32_ref; - module->vtable.vec_rnx_from_tnx32 = vec_rnx_from_tnx32_ref; - module->vtable.vec_rnx_to_tnxdbl = vec_rnx_to_tnxdbl_ref; - module->vtable.bytes_of_rnx_svp_ppol = fft64_bytes_of_rnx_svp_ppol; - module->vtable.rnx_svp_prepare = fft64_rnx_svp_prepare_ref; - module->vtable.rnx_svp_apply = fft64_rnx_svp_apply_ref; - - // Add optimized function pointers here - if (CPU_SUPPORTS("avx")) { - module->vtable.vec_rnx_add = vec_rnx_add_avx; - module->vtable.vec_rnx_sub = vec_rnx_sub_avx; - module->vtable.vec_rnx_negate = vec_rnx_negate_avx; - module->vtable.rnx_vmp_apply_dft_to_dft_tmp_bytes = fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_avx; - module->vtable.rnx_vmp_apply_dft_to_dft = fft64_rnx_vmp_apply_dft_to_dft_avx; - module->vtable.rnx_vmp_apply_tmp_a_tmp_bytes = fft64_rnx_vmp_apply_tmp_a_tmp_bytes_avx; - module->vtable.rnx_vmp_apply_tmp_a = fft64_rnx_vmp_apply_tmp_a_avx; - module->vtable.rnx_vmp_prepare_contiguous_tmp_bytes = fft64_rnx_vmp_prepare_contiguous_tmp_bytes_avx; - module->vtable.rnx_vmp_prepare_contiguous = fft64_rnx_vmp_prepare_contiguous_avx; - module->vtable.rnx_approxdecomp_from_tnxdbl = rnx_approxdecomp_from_tnxdbl_avx; - } -} - -void init_rnx_module_info(MOD_RNX* module, // - uint64_t n, RNX_MODULE_TYPE mtype) { - memset(module, 0, sizeof(MOD_RNX)); - module->n = n; - module->m = n >> 1; - module->mtype = mtype; - switch (mtype) { - case FFT64: - fft64_init_rnx_module_precomp(module); - fft64_init_rnx_module_vtable(module); - break; - default: - NOT_SUPPORTED(); // unknown mtype - } -} - -void finalize_rnx_module_info(MOD_RNX* module) { - if (module->custom) module->custom_deleter(module->custom); - switch (module->mtype) { - case FFT64: - fft64_finalize_rnx_module_precomp(module); - // fft64_finalize_rnx_module_vtable(module); // nothing to finalize - break; - default: - NOT_SUPPORTED(); // unknown mtype - } -} - -EXPORT MOD_RNX* new_rnx_module_info(uint64_t nn, RNX_MODULE_TYPE mtype) { - MOD_RNX* res = (MOD_RNX*)malloc(sizeof(MOD_RNX)); - init_rnx_module_info(res, nn, mtype); - return res; -} - -EXPORT void delete_rnx_module_info(MOD_RNX* module_info) { - finalize_rnx_module_info(module_info); - free(module_info); -} - -EXPORT uint64_t rnx_module_get_n(const MOD_RNX* module) { return module->n; } - -/** @brief allocates a prepared matrix (release with delete_rnx_vmp_pmat) */ -EXPORT RNX_VMP_PMAT* new_rnx_vmp_pmat(const MOD_RNX* module, // N - uint64_t nrows, uint64_t ncols) { // dimensions - return (RNX_VMP_PMAT*)spqlios_alloc(bytes_of_rnx_vmp_pmat(module, nrows, ncols)); -} -EXPORT void delete_rnx_vmp_pmat(RNX_VMP_PMAT* ptr) { spqlios_free(ptr); } - -//////////////// wrappers ////////////////// - -/** @brief sets res = a + b */ -EXPORT void vec_rnx_add( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -) { - module->vtable.vec_rnx_add(module, res, res_size, res_sl, a, a_size, a_sl, b, b_size, b_sl); -} - -/** @brief sets res = 0 */ -EXPORT void vec_rnx_zero( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl // res -) { - module->vtable.vec_rnx_zero(module, res, res_size, res_sl); -} - -/** @brief sets res = a */ -EXPORT void vec_rnx_copy( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - module->vtable.vec_rnx_copy(module, res, res_size, res_sl, a, a_size, a_sl); -} - -/** @brief sets res = -a */ -EXPORT void vec_rnx_negate( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - module->vtable.vec_rnx_negate(module, res, res_size, res_sl, a, a_size, a_sl); -} - -/** @brief sets res = a - b */ -EXPORT void vec_rnx_sub( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -) { - module->vtable.vec_rnx_sub(module, res, res_size, res_sl, a, a_size, a_sl, b, b_size, b_sl); -} - -/** @brief sets res = a . X^p */ -EXPORT void vec_rnx_rotate( // - const MOD_RNX* module, // N - const int64_t p, // rotation value - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - module->vtable.vec_rnx_rotate(module, p, res, res_size, res_sl, a, a_size, a_sl); -} - -/** @brief sets res = a(X^p) */ -EXPORT void vec_rnx_automorphism( // - const MOD_RNX* module, // N - int64_t p, // X -> X^p - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - module->vtable.vec_rnx_automorphism(module, p, res, res_size, res_sl, a, a_size, a_sl); -} - -EXPORT void vec_rnx_mul_xp_minus_one( // - const MOD_RNX* module, // N - const int64_t p, // rotation value - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - module->vtable.vec_rnx_mul_xp_minus_one(module, p, res, res_size, res_sl, a, a_size, a_sl); -} -/** @brief number of bytes in a RNX_VMP_PMAT (for manual allocation) */ -EXPORT uint64_t bytes_of_rnx_vmp_pmat(const MOD_RNX* module, // N - uint64_t nrows, uint64_t ncols) { // dimensions - return module->vtable.bytes_of_rnx_vmp_pmat(module, nrows, ncols); -} - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void rnx_vmp_prepare_contiguous( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double* a, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -) { - module->vtable.rnx_vmp_prepare_contiguous(module, pmat, a, nrows, ncols, tmp_space); -} - -/** @brief number of scratch bytes necessary to prepare a matrix */ -EXPORT uint64_t rnx_vmp_prepare_contiguous_tmp_bytes(const MOD_RNX* module) { - return module->vtable.rnx_vmp_prepare_contiguous_tmp_bytes(module); -} - -/** @brief applies a vmp product res = a x pmat */ -EXPORT void rnx_vmp_apply_tmp_a( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - double* tmpa, uint64_t a_size, uint64_t a_sl, // a (will be overwritten) - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -) { - module->vtable.rnx_vmp_apply_tmp_a(module, res, res_size, res_sl, tmpa, a_size, a_sl, pmat, nrows, ncols, tmp_space); -} - -EXPORT uint64_t rnx_vmp_apply_tmp_a_tmp_bytes( // - const MOD_RNX* module, // N - uint64_t res_size, // res size - uint64_t a_size, // a size - uint64_t nrows, uint64_t ncols // prep matrix dims -) { - return module->vtable.rnx_vmp_apply_tmp_a_tmp_bytes(module, res_size, a_size, nrows, ncols); -} - -/** @brief minimal size of the tmp_space */ -EXPORT void rnx_vmp_apply_dft_to_dft( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a_dft, uint64_t a_size, uint64_t a_sl, // a - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -) { - module->vtable.rnx_vmp_apply_dft_to_dft(module, res, res_size, res_sl, a_dft, a_size, a_sl, pmat, nrows, ncols, - tmp_space); -} - -/** @brief minimal size of the tmp_space */ -EXPORT uint64_t rnx_vmp_apply_dft_to_dft_tmp_bytes( // - const MOD_RNX* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -) { - return module->vtable.rnx_vmp_apply_dft_to_dft_tmp_bytes(module, res_size, a_size, nrows, ncols); -} - -EXPORT uint64_t bytes_of_rnx_svp_ppol(const MOD_RNX* module) { return module->vtable.bytes_of_rnx_svp_ppol(module); } - -EXPORT void rnx_svp_prepare(const MOD_RNX* module, // N - RNX_SVP_PPOL* ppol, // output - const double* pol // a -) { - module->vtable.rnx_svp_prepare(module, ppol, pol); -} - -EXPORT void rnx_svp_apply( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // output - const RNX_SVP_PPOL* ppol, // prepared pol - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - module->vtable.rnx_svp_apply(module, // N - res, res_size, res_sl, // output - ppol, // prepared pol - a, a_size, a_sl); -} - -EXPORT void rnx_approxdecomp_from_tnxdbl( // - const MOD_RNX* module, // N - const TNXDBL_APPROXDECOMP_GADGET* gadget, // output base 2^K - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a) { // a - module->vtable.rnx_approxdecomp_from_tnxdbl(module, gadget, res, res_size, res_sl, a); -} - -EXPORT void vec_rnx_to_znx32( // - const MOD_RNX* module, // N - int32_t* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - module->vtable.vec_rnx_to_znx32(module, res, res_size, res_sl, a, a_size, a_sl); -} - -EXPORT void vec_rnx_from_znx32( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const int32_t* a, uint64_t a_size, uint64_t a_sl // a -) { - module->vtable.vec_rnx_from_znx32(module, res, res_size, res_sl, a, a_size, a_sl); -} - -EXPORT void vec_rnx_to_tnx32( // - const MOD_RNX* module, // N - int32_t* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - module->vtable.vec_rnx_to_tnx32(module, res, res_size, res_sl, a, a_size, a_sl); -} - -EXPORT void vec_rnx_from_tnx32( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const int32_t* a, uint64_t a_size, uint64_t a_sl // a -) { - module->vtable.vec_rnx_from_tnx32(module, res, res_size, res_sl, a, a_size, a_sl); -} - -EXPORT void vec_rnx_to_tnxdbl( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - module->vtable.vec_rnx_to_tnxdbl(module, res, res_size, res_sl, a, a_size, a_sl); -}
D spqlios/lib/spqlios/arithmetic/vec_rnx_approxdecomp_avx.c

@@ -1,59 +0,0 @@

-#include <memory.h> - -#include "immintrin.h" -#include "vec_rnx_arithmetic_private.h" - -/** @brief sets res = gadget_decompose(a) */ -EXPORT void rnx_approxdecomp_from_tnxdbl_avx( // - const MOD_RNX* module, // N - const TNXDBL_APPROXDECOMP_GADGET* gadget, // output base 2^K - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a // a -) { - const uint64_t nn = module->n; - if (nn < 4) return rnx_approxdecomp_from_tnxdbl_ref(module, gadget, res, res_size, res_sl, a); - const uint64_t ell = gadget->ell; - const __m256i k = _mm256_set1_epi64x(gadget->k); - const __m256d add_cst = _mm256_set1_pd(gadget->add_cst); - const __m256i and_mask = _mm256_set1_epi64x(gadget->and_mask); - const __m256i or_mask = _mm256_set1_epi64x(gadget->or_mask); - const __m256d sub_cst = _mm256_set1_pd(gadget->sub_cst); - const uint64_t msize = res_size <= ell ? res_size : ell; - // gadget decompose column by column - if (msize == ell) { - // this is the main scenario when msize == ell - double* const last_r = res + (msize - 1) * res_sl; - for (uint64_t j = 0; j < nn; j += 4) { - double* rr = last_r + j; - const double* aa = a + j; - __m256d t_dbl = _mm256_add_pd(_mm256_loadu_pd(aa), add_cst); - __m256i t_int = _mm256_castpd_si256(t_dbl); - do { - __m256i u_int = _mm256_or_si256(_mm256_and_si256(t_int, and_mask), or_mask); - _mm256_storeu_pd(rr, _mm256_sub_pd(_mm256_castsi256_pd(u_int), sub_cst)); - t_int = _mm256_srlv_epi64(t_int, k); - rr -= res_sl; - } while (rr >= res); - } - } else if (msize > 0) { - // otherwise, if msize < ell: there is one additional rshift - const __m256i first_rsh = _mm256_set1_epi64x((ell - msize) * gadget->k); - double* const last_r = res + (msize - 1) * res_sl; - for (uint64_t j = 0; j < nn; j += 4) { - double* rr = last_r + j; - const double* aa = a + j; - __m256d t_dbl = _mm256_add_pd(_mm256_loadu_pd(aa), add_cst); - __m256i t_int = _mm256_srlv_epi64(_mm256_castpd_si256(t_dbl), first_rsh); - do { - __m256i u_int = _mm256_or_si256(_mm256_and_si256(t_int, and_mask), or_mask); - _mm256_storeu_pd(rr, _mm256_sub_pd(_mm256_castsi256_pd(u_int), sub_cst)); - t_int = _mm256_srlv_epi64(t_int, k); - rr -= res_sl; - } while (rr >= res); - } - } - // zero-out the last slices (if any) - for (uint64_t i = msize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -}
D spqlios/lib/spqlios/arithmetic/vec_rnx_approxdecomp_ref.c

@@ -1,75 +0,0 @@

-#include <memory.h> - -#include "vec_rnx_arithmetic_private.h" - -typedef union di { - double dv; - uint64_t uv; -} di_t; - -/** @brief new gadget: delete with delete_tnxdbl_approxdecomp_gadget */ -EXPORT TNXDBL_APPROXDECOMP_GADGET* new_tnxdbl_approxdecomp_gadget( // - const MOD_RNX* module, // N - uint64_t k, uint64_t ell // base 2^K and size -) { - if (k * ell > 50) return spqlios_error("gadget requires a too large fp precision"); - TNXDBL_APPROXDECOMP_GADGET* res = spqlios_alloc(sizeof(TNXDBL_APPROXDECOMP_GADGET)); - res->k = k; - res->ell = ell; - // double add_cst; // double(3.2^(51-ell.K) + 1/2.(sum 2^(-iK)) for i=[0,ell[) - union di add_cst; - add_cst.dv = UINT64_C(3) << (51 - ell * k); - for (uint64_t i = 0; i < ell; ++i) { - add_cst.uv |= UINT64_C(1) << ((i + 1) * k - 1); - } - res->add_cst = add_cst.dv; - // uint64_t and_mask; // uint64(2^(K)-1) - res->and_mask = (UINT64_C(1) << k) - 1; - // uint64_t or_mask; // double(2^52) - union di or_mask; - or_mask.dv = (UINT64_C(1) << 52); - res->or_mask = or_mask.uv; - // double sub_cst; // double(2^52 + 2^(K-1)) - res->sub_cst = ((UINT64_C(1) << 52) + (UINT64_C(1) << (k - 1))); - return res; -} - -EXPORT void delete_tnxdbl_approxdecomp_gadget(TNXDBL_APPROXDECOMP_GADGET* gadget) { spqlios_free(gadget); } - -/** @brief sets res = gadget_decompose(a) */ -EXPORT void rnx_approxdecomp_from_tnxdbl_ref( // - const MOD_RNX* module, // N - const TNXDBL_APPROXDECOMP_GADGET* gadget, // output base 2^K - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a // a -) { - const uint64_t nn = module->n; - const uint64_t k = gadget->k; - const uint64_t ell = gadget->ell; - const double add_cst = gadget->add_cst; - const uint64_t and_mask = gadget->and_mask; - const uint64_t or_mask = gadget->or_mask; - const double sub_cst = gadget->sub_cst; - const uint64_t msize = res_size <= ell ? res_size : ell; - const uint64_t first_rsh = (ell - msize) * k; - // gadget decompose column by column - if (msize > 0) { - double* const last_r = res + (msize - 1) * res_sl; - for (uint64_t j = 0; j < nn; ++j) { - double* rr = last_r + j; - di_t t = {.dv = a[j] + add_cst}; - if (msize < ell) t.uv >>= first_rsh; - do { - di_t u; - u.uv = (t.uv & and_mask) | or_mask; - *rr = u.dv - sub_cst; - t.uv >>= k; - rr -= res_sl; - } while (rr >= res); - } - } - // zero-out the last slices (if any) - for (uint64_t i = msize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -}
D spqlios/lib/spqlios/arithmetic/vec_rnx_arithmetic.c

@@ -1,223 +0,0 @@

-#include <string.h> - -#include "../coeffs/coeffs_arithmetic.h" -#include "vec_rnx_arithmetic_private.h" - -void rnx_add_ref(uint64_t nn, double* res, const double* a, const double* b) { - for (uint64_t i = 0; i < nn; ++i) { - res[i] = a[i] + b[i]; - } -} - -void rnx_sub_ref(uint64_t nn, double* res, const double* a, const double* b) { - for (uint64_t i = 0; i < nn; ++i) { - res[i] = a[i] - b[i]; - } -} - -void rnx_negate_ref(uint64_t nn, double* res, const double* a) { - for (uint64_t i = 0; i < nn; ++i) { - res[i] = -a[i]; - } -} - -/** @brief sets res = a + b */ -EXPORT void vec_rnx_add_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t nn = module->n; - if (a_size < b_size) { - const uint64_t msize = res_size < a_size ? res_size : a_size; - const uint64_t nsize = res_size < b_size ? res_size : b_size; - for (uint64_t i = 0; i < msize; ++i) { - rnx_add_ref(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - for (uint64_t i = msize; i < nsize; ++i) { - memcpy(res + i * res_sl, b + i * b_sl, nn * sizeof(double)); - } - for (uint64_t i = nsize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } - } else { - const uint64_t msize = res_size < b_size ? res_size : b_size; - const uint64_t nsize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - rnx_add_ref(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - for (uint64_t i = msize; i < nsize; ++i) { - memcpy(res + i * res_sl, a + i * a_sl, nn * sizeof(double)); - } - for (uint64_t i = nsize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } - } -} - -/** @brief sets res = 0 */ -EXPORT void vec_rnx_zero_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl // res -) { - const uint64_t nn = module->n; - for (uint64_t i = 0; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -} - -/** @brief sets res = a */ -EXPORT void vec_rnx_copy_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - - const uint64_t rot_end_idx = res_size < a_size ? res_size : a_size; - // rotate up to the smallest dimension - for (uint64_t i = 0; i < rot_end_idx; ++i) { - double* res_ptr = res + i * res_sl; - const double* a_ptr = a + i * a_sl; - memcpy(res_ptr, a_ptr, nn * sizeof(double)); - } - // then extend with zeros - for (uint64_t i = rot_end_idx; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -} - -/** @brief sets res = -a */ -EXPORT void vec_rnx_negate_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - - const uint64_t rot_end_idx = res_size < a_size ? res_size : a_size; - // rotate up to the smallest dimension - for (uint64_t i = 0; i < rot_end_idx; ++i) { - double* res_ptr = res + i * res_sl; - const double* a_ptr = a + i * a_sl; - rnx_negate_ref(nn, res_ptr, a_ptr); - } - // then extend with zeros - for (uint64_t i = rot_end_idx; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -} - -/** @brief sets res = a - b */ -EXPORT void vec_rnx_sub_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t nn = module->n; - if (a_size < b_size) { - const uint64_t msize = res_size < a_size ? res_size : a_size; - const uint64_t nsize = res_size < b_size ? res_size : b_size; - for (uint64_t i = 0; i < msize; ++i) { - rnx_sub_ref(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - for (uint64_t i = msize; i < nsize; ++i) { - rnx_negate_ref(nn, res + i * res_sl, b + i * b_sl); - } - for (uint64_t i = nsize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } - } else { - const uint64_t msize = res_size < b_size ? res_size : b_size; - const uint64_t nsize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - rnx_sub_ref(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - for (uint64_t i = msize; i < nsize; ++i) { - memcpy(res + i * res_sl, a + i * a_sl, nn * sizeof(double)); - } - for (uint64_t i = nsize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } - } -} - -/** @brief sets res = a . X^p */ -EXPORT void vec_rnx_rotate_ref( // - const MOD_RNX* module, // N - const int64_t p, // rotation value - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - - const uint64_t rot_end_idx = res_size < a_size ? res_size : a_size; - // rotate up to the smallest dimension - for (uint64_t i = 0; i < rot_end_idx; ++i) { - double* res_ptr = res + i * res_sl; - const double* a_ptr = a + i * a_sl; - if (res_ptr == a_ptr) { - rnx_rotate_inplace_f64(nn, p, res_ptr); - } else { - rnx_rotate_f64(nn, p, res_ptr, a_ptr); - } - } - // then extend with zeros - for (uint64_t i = rot_end_idx; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -} - -/** @brief sets res = a(X^p) */ -EXPORT void vec_rnx_automorphism_ref( // - const MOD_RNX* module, // N - int64_t p, // X -> X^p - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - - const uint64_t rot_end_idx = res_size < a_size ? res_size : a_size; - // rotate up to the smallest dimension - for (uint64_t i = 0; i < rot_end_idx; ++i) { - double* res_ptr = res + i * res_sl; - const double* a_ptr = a + i * a_sl; - if (res_ptr == a_ptr) { - rnx_automorphism_inplace_f64(nn, p, res_ptr); - } else { - rnx_automorphism_f64(nn, p, res_ptr, a_ptr); - } - } - // then extend with zeros - for (uint64_t i = rot_end_idx; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -} - -/** @brief sets res = a . (X^p - 1) */ -EXPORT void vec_rnx_mul_xp_minus_one_ref( // - const MOD_RNX* module, // N - const int64_t p, // rotation value - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - - const uint64_t rot_end_idx = res_size < a_size ? res_size : a_size; - // rotate up to the smallest dimension - for (uint64_t i = 0; i < rot_end_idx; ++i) { - double* res_ptr = res + i * res_sl; - const double* a_ptr = a + i * a_sl; - if (res_ptr == a_ptr) { - rnx_mul_xp_minus_one_inplace(nn, p, res_ptr); - } else { - rnx_mul_xp_minus_one(nn, p, res_ptr, a_ptr); - } - } - // then extend with zeros - for (uint64_t i = rot_end_idx; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -}
D spqlios/lib/spqlios/arithmetic/vec_rnx_arithmetic.h

@@ -1,340 +0,0 @@

-#ifndef SPQLIOS_VEC_RNX_ARITHMETIC_H -#define SPQLIOS_VEC_RNX_ARITHMETIC_H - -#include <stdint.h> - -#include "../commons.h" - -/** - * We support the following module families: - * - FFT64: - * the overall precision should fit at all times over 52 bits. - */ -typedef enum rnx_module_type_t { FFT64 } RNX_MODULE_TYPE; - -/** @brief opaque structure that describes the modules (RnX,ZnX,TnX) and the hardware */ -typedef struct rnx_module_info_t MOD_RNX; - -/** - * @brief obtain a module info for ring dimension N - * the module-info knows about: - * - the dimension N (or the complex dimension m=N/2) - * - any moduleuted fft or ntt items - * - the hardware (avx, arm64, x86, ...) - */ -EXPORT MOD_RNX* new_rnx_module_info(uint64_t N, RNX_MODULE_TYPE mode); -EXPORT void delete_rnx_module_info(MOD_RNX* module_info); -EXPORT uint64_t rnx_module_get_n(const MOD_RNX* module); - -// basic arithmetic - -/** @brief sets res = 0 */ -EXPORT void vec_rnx_zero( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl // res -); - -/** @brief sets res = a */ -EXPORT void vec_rnx_copy( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = -a */ -EXPORT void vec_rnx_negate( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = a + b */ -EXPORT void vec_rnx_add( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -); - -/** @brief sets res = a - b */ -EXPORT void vec_rnx_sub( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -); - -/** @brief sets res = a . X^p */ -EXPORT void vec_rnx_rotate( // - const MOD_RNX* module, // N - const int64_t p, // rotation value - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = a . (X^p - 1) */ -EXPORT void vec_rnx_mul_xp_minus_one( // - const MOD_RNX* module, // N - const int64_t p, // rotation value - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = a(X^p) */ -EXPORT void vec_rnx_automorphism( // - const MOD_RNX* module, // N - int64_t p, // X -> X^p - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/////////////////////////////////////////////////////////////////// -// conversions // -/////////////////////////////////////////////////////////////////// - -EXPORT void vec_rnx_to_znx32( // - const MOD_RNX* module, // N - int32_t* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_rnx_from_znx32( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const int32_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_rnx_to_tnx32( // - const MOD_RNX* module, // N - int32_t* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_rnx_from_tnx32( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const int32_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_rnx_to_tnx32x2( // - const MOD_RNX* module, // N - int32_t* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_rnx_from_tnx32x2( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const int32_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_rnx_to_tnxdbl( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/////////////////////////////////////////////////////////////////// -// isolated products (n.log(n), but not particularly optimized // -/////////////////////////////////////////////////////////////////// - -/** @brief res = a * b : small polynomial product */ -EXPORT void rnx_small_single_product( // - const MOD_RNX* module, // N - double* res, // output - const double* a, // a - const double* b, // b - uint8_t* tmp); // scratch space - -EXPORT uint64_t rnx_small_single_product_tmp_bytes(const MOD_RNX* module); - -/** @brief res = a * b centermod 1: small polynomial product */ -EXPORT void tnxdbl_small_single_product( // - const MOD_RNX* module, // N - double* torus_res, // output - const double* int_a, // a - const double* torus_b, // b - uint8_t* tmp); // scratch space - -EXPORT uint64_t tnxdbl_small_single_product_tmp_bytes(const MOD_RNX* module); - -/** @brief res = a * b: small polynomial product */ -EXPORT void znx32_small_single_product( // - const MOD_RNX* module, // N - int32_t* int_res, // output - const int32_t* int_a, // a - const int32_t* int_b, // b - uint8_t* tmp); // scratch space - -EXPORT uint64_t znx32_small_single_product_tmp_bytes(const MOD_RNX* module); - -/** @brief res = a * b centermod 1: small polynomial product */ -EXPORT void tnx32_small_single_product( // - const MOD_RNX* module, // N - int32_t* torus_res, // output - const int32_t* int_a, // a - const int32_t* torus_b, // b - uint8_t* tmp); // scratch space - -EXPORT uint64_t tnx32_small_single_product_tmp_bytes(const MOD_RNX* module); - -/////////////////////////////////////////////////////////////////// -// prepared gadget decompositions (optimized) // -/////////////////////////////////////////////////////////////////// - -// decompose from tnx32 - -typedef struct tnx32_approxdecomp_gadget_t TNX32_APPROXDECOMP_GADGET; - -/** @brief new gadget: delete with delete_tnx32_approxdecomp_gadget */ -EXPORT TNX32_APPROXDECOMP_GADGET* new_tnx32_approxdecomp_gadget( // - const MOD_RNX* module, // N - uint64_t k, uint64_t ell // base 2^K and size -); -EXPORT void delete_tnx32_approxdecomp_gadget(const MOD_RNX* module); - -/** @brief sets res = gadget_decompose(a) */ -EXPORT void rnx_approxdecomp_from_tnx32( // - const MOD_RNX* module, // N - const TNX32_APPROXDECOMP_GADGET* gadget, // output base 2^K - double* res, uint64_t res_size, uint64_t res_sl, // res - const int32_t* a // a -); - -// decompose from tnx32x2 - -typedef struct tnx32x2_approxdecomp_gadget_t TNX32X2_APPROXDECOMP_GADGET; - -/** @brief new gadget: delete with delete_tnx32x2_approxdecomp_gadget */ -EXPORT TNX32X2_APPROXDECOMP_GADGET* new_tnx32x2_approxdecomp_gadget(const MOD_RNX* module, uint64_t ka, uint64_t ella, - uint64_t kb, uint64_t ellb); -EXPORT void delete_tnx32x2_approxdecomp_gadget(const MOD_RNX* module); - -/** @brief sets res = gadget_decompose(a) */ -EXPORT void rnx_approxdecomp_from_tnx32x2( // - const MOD_RNX* module, // N - const TNX32X2_APPROXDECOMP_GADGET* gadget, // output base 2^K - double* res, uint64_t res_size, uint64_t res_sl, // res - const int32_t* a // a -); - -// decompose from tnxdbl - -typedef struct tnxdbl_approxdecomp_gadget_t TNXDBL_APPROXDECOMP_GADGET; - -/** @brief new gadget: delete with delete_tnxdbl_approxdecomp_gadget */ -EXPORT TNXDBL_APPROXDECOMP_GADGET* new_tnxdbl_approxdecomp_gadget( // - const MOD_RNX* module, // N - uint64_t k, uint64_t ell // base 2^K and size -); -EXPORT void delete_tnxdbl_approxdecomp_gadget(TNXDBL_APPROXDECOMP_GADGET* gadget); - -/** @brief sets res = gadget_decompose(a) */ -EXPORT void rnx_approxdecomp_from_tnxdbl( // - const MOD_RNX* module, // N - const TNXDBL_APPROXDECOMP_GADGET* gadget, // output base 2^K - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a); // a - -/////////////////////////////////////////////////////////////////// -// prepared scalar-vector product (optimized) // -/////////////////////////////////////////////////////////////////// - -/** @brief opaque type that represents a polynomial of RnX prepared for a scalar-vector product */ -typedef struct rnx_svp_ppol_t RNX_SVP_PPOL; - -/** @brief number of bytes in a RNX_VMP_PMAT (for manual allocation) */ -EXPORT uint64_t bytes_of_rnx_svp_ppol(const MOD_RNX* module); // N - -/** @brief allocates a prepared vector (release with delete_rnx_svp_ppol) */ -EXPORT RNX_SVP_PPOL* new_rnx_svp_ppol(const MOD_RNX* module); // N - -/** @brief frees memory for a prepared vector */ -EXPORT void delete_rnx_svp_ppol(RNX_SVP_PPOL* res); - -/** @brief prepares a svp polynomial */ -EXPORT void rnx_svp_prepare(const MOD_RNX* module, // N - RNX_SVP_PPOL* ppol, // output - const double* pol // a -); - -/** @brief apply a svp product, result = ppol * a, presented in DFT space */ -EXPORT void rnx_svp_apply( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // output - const RNX_SVP_PPOL* ppol, // prepared pol - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/////////////////////////////////////////////////////////////////// -// prepared vector-matrix product (optimized) // -/////////////////////////////////////////////////////////////////// - -typedef struct rnx_vmp_pmat_t RNX_VMP_PMAT; - -/** @brief number of bytes in a RNX_VMP_PMAT (for manual allocation) */ -EXPORT uint64_t bytes_of_rnx_vmp_pmat(const MOD_RNX* module, // N - uint64_t nrows, uint64_t ncols); // dimensions - -/** @brief allocates a prepared matrix (release with delete_rnx_vmp_pmat) */ -EXPORT RNX_VMP_PMAT* new_rnx_vmp_pmat(const MOD_RNX* module, // N - uint64_t nrows, uint64_t ncols); // dimensions -EXPORT void delete_rnx_vmp_pmat(RNX_VMP_PMAT* ptr); - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void rnx_vmp_prepare_contiguous( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double* a, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -); - -/** @brief number of scratch bytes necessary to prepare a matrix */ -EXPORT uint64_t rnx_vmp_prepare_contiguous_tmp_bytes(const MOD_RNX* module); - -/** @brief applies a vmp product res = a x pmat */ -EXPORT void rnx_vmp_apply_tmp_a( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - double* tmpa, uint64_t a_size, uint64_t a_sl, // a (will be overwritten) - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -); - -EXPORT uint64_t rnx_vmp_apply_tmp_a_tmp_bytes( // - const MOD_RNX* module, // N - uint64_t res_size, // res size - uint64_t a_size, // a size - uint64_t nrows, uint64_t ncols // prep matrix dims -); - -/** @brief minimal size of the tmp_space */ -EXPORT void rnx_vmp_apply_dft_to_dft( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a_dft, uint64_t a_size, uint64_t a_sl, // a - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -); - -/** @brief minimal size of the tmp_space */ -EXPORT uint64_t rnx_vmp_apply_dft_to_dft_tmp_bytes( // - const MOD_RNX* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -); - -/** @brief sets res = DFT(a) */ -EXPORT void vec_rnx_dft(const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = iDFT(a_dft) -- idft is not normalized */ -EXPORT void vec_rnx_idft(const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a_dft, uint64_t a_size, uint64_t a_sl // a -); - -#endif // SPQLIOS_VEC_RNX_ARITHMETIC_H
D spqlios/lib/spqlios/arithmetic/vec_rnx_arithmetic_avx.c

@@ -1,189 +0,0 @@

-#include <immintrin.h> -#include <string.h> - -#include "vec_rnx_arithmetic_private.h" - -void rnx_add_avx(uint64_t nn, double* res, const double* a, const double* b) { - if (nn < 8) { - if (nn == 4) { - _mm256_storeu_pd(res, _mm256_add_pd(_mm256_loadu_pd(a), _mm256_loadu_pd(b))); - } else if (nn == 2) { - _mm_storeu_pd(res, _mm_add_pd(_mm_loadu_pd(a), _mm_loadu_pd(b))); - } else if (nn == 1) { - *res = *a + *b; - } else { - NOT_SUPPORTED(); // not a power of 2 - } - return; - } - // general case: nn >= 8 - __m256d x0, x1, x2, x3, x4, x5; - const double* aa = a; - const double* bb = b; - double* rr = res; - double* const rrend = res + nn; - do { - x0 = _mm256_loadu_pd(aa); - x1 = _mm256_loadu_pd(aa + 4); - x2 = _mm256_loadu_pd(bb); - x3 = _mm256_loadu_pd(bb + 4); - x4 = _mm256_add_pd(x0, x2); - x5 = _mm256_add_pd(x1, x3); - _mm256_storeu_pd(rr, x4); - _mm256_storeu_pd(rr + 4, x5); - aa += 8; - bb += 8; - rr += 8; - } while (rr < rrend); -} - -void rnx_sub_avx(uint64_t nn, double* res, const double* a, const double* b) { - if (nn < 8) { - if (nn == 4) { - _mm256_storeu_pd(res, _mm256_sub_pd(_mm256_loadu_pd(a), _mm256_loadu_pd(b))); - } else if (nn == 2) { - _mm_storeu_pd(res, _mm_sub_pd(_mm_loadu_pd(a), _mm_loadu_pd(b))); - } else if (nn == 1) { - *res = *a - *b; - } else { - NOT_SUPPORTED(); // not a power of 2 - } - return; - } - // general case: nn >= 8 - __m256d x0, x1, x2, x3, x4, x5; - const double* aa = a; - const double* bb = b; - double* rr = res; - double* const rrend = res + nn; - do { - x0 = _mm256_loadu_pd(aa); - x1 = _mm256_loadu_pd(aa + 4); - x2 = _mm256_loadu_pd(bb); - x3 = _mm256_loadu_pd(bb + 4); - x4 = _mm256_sub_pd(x0, x2); - x5 = _mm256_sub_pd(x1, x3); - _mm256_storeu_pd(rr, x4); - _mm256_storeu_pd(rr + 4, x5); - aa += 8; - bb += 8; - rr += 8; - } while (rr < rrend); -} - -void rnx_negate_avx(uint64_t nn, double* res, const double* b) { - if (nn < 8) { - if (nn == 4) { - _mm256_storeu_pd(res, _mm256_sub_pd(_mm256_set1_pd(0), _mm256_loadu_pd(b))); - } else if (nn == 2) { - _mm_storeu_pd(res, _mm_sub_pd(_mm_set1_pd(0), _mm_loadu_pd(b))); - } else if (nn == 1) { - *res = -*b; - } else { - NOT_SUPPORTED(); // not a power of 2 - } - return; - } - // general case: nn >= 8 - __m256d x2, x3, x4, x5; - const __m256d ZERO = _mm256_set1_pd(0); - const double* bb = b; - double* rr = res; - double* const rrend = res + nn; - do { - x2 = _mm256_loadu_pd(bb); - x3 = _mm256_loadu_pd(bb + 4); - x4 = _mm256_sub_pd(ZERO, x2); - x5 = _mm256_sub_pd(ZERO, x3); - _mm256_storeu_pd(rr, x4); - _mm256_storeu_pd(rr + 4, x5); - bb += 8; - rr += 8; - } while (rr < rrend); -} - -/** @brief sets res = a + b */ -EXPORT void vec_rnx_add_avx( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t nn = module->n; - if (a_size < b_size) { - const uint64_t msize = res_size < a_size ? res_size : a_size; - const uint64_t nsize = res_size < b_size ? res_size : b_size; - for (uint64_t i = 0; i < msize; ++i) { - rnx_add_avx(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - for (uint64_t i = msize; i < nsize; ++i) { - memcpy(res + i * res_sl, b + i * b_sl, nn * sizeof(double)); - } - for (uint64_t i = nsize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } - } else { - const uint64_t msize = res_size < b_size ? res_size : b_size; - const uint64_t nsize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - rnx_add_avx(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - for (uint64_t i = msize; i < nsize; ++i) { - memcpy(res + i * res_sl, a + i * a_sl, nn * sizeof(double)); - } - for (uint64_t i = nsize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } - } -} - -/** @brief sets res = -a */ -EXPORT void vec_rnx_negate_avx( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - rnx_negate_avx(nn, res + i * res_sl, a + i * a_sl); - } - for (uint64_t i = msize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -} - -/** @brief sets res = a - b */ -EXPORT void vec_rnx_sub_avx( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t nn = module->n; - if (a_size < b_size) { - const uint64_t msize = res_size < a_size ? res_size : a_size; - const uint64_t nsize = res_size < b_size ? res_size : b_size; - for (uint64_t i = 0; i < msize; ++i) { - rnx_sub_avx(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - for (uint64_t i = msize; i < nsize; ++i) { - rnx_negate_avx(nn, res + i * res_sl, b + i * b_sl); - } - for (uint64_t i = nsize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } - } else { - const uint64_t msize = res_size < b_size ? res_size : b_size; - const uint64_t nsize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - rnx_sub_avx(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - for (uint64_t i = msize; i < nsize; ++i) { - memcpy(res + i * res_sl, a + i * a_sl, nn * sizeof(double)); - } - for (uint64_t i = nsize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } - } -}
D spqlios/lib/spqlios/arithmetic/vec_rnx_arithmetic_plugin.h

@@ -1,88 +0,0 @@

-#ifndef SPQLIOS_VEC_RNX_ARITHMETIC_PLUGIN_H -#define SPQLIOS_VEC_RNX_ARITHMETIC_PLUGIN_H - -#include "vec_rnx_arithmetic.h" - -typedef typeof(vec_rnx_zero) VEC_RNX_ZERO_F; -typedef typeof(vec_rnx_copy) VEC_RNX_COPY_F; -typedef typeof(vec_rnx_negate) VEC_RNX_NEGATE_F; -typedef typeof(vec_rnx_add) VEC_RNX_ADD_F; -typedef typeof(vec_rnx_sub) VEC_RNX_SUB_F; -typedef typeof(vec_rnx_rotate) VEC_RNX_ROTATE_F; -typedef typeof(vec_rnx_mul_xp_minus_one) VEC_RNX_MUL_XP_MINUS_ONE_F; -typedef typeof(vec_rnx_automorphism) VEC_RNX_AUTOMORPHISM_F; -typedef typeof(vec_rnx_to_znx32) VEC_RNX_TO_ZNX32_F; -typedef typeof(vec_rnx_from_znx32) VEC_RNX_FROM_ZNX32_F; -typedef typeof(vec_rnx_to_tnx32) VEC_RNX_TO_TNX32_F; -typedef typeof(vec_rnx_from_tnx32) VEC_RNX_FROM_TNX32_F; -typedef typeof(vec_rnx_to_tnx32x2) VEC_RNX_TO_TNX32X2_F; -typedef typeof(vec_rnx_from_tnx32x2) VEC_RNX_FROM_TNX32X2_F; -typedef typeof(vec_rnx_to_tnxdbl) VEC_RNX_TO_TNXDBL_F; -// typedef typeof(vec_rnx_from_tnxdbl) VEC_RNX_FROM_TNXDBL_F; -typedef typeof(rnx_small_single_product) RNX_SMALL_SINGLE_PRODUCT_F; -typedef typeof(rnx_small_single_product_tmp_bytes) RNX_SMALL_SINGLE_PRODUCT_TMP_BYTES_F; -typedef typeof(tnxdbl_small_single_product) TNXDBL_SMALL_SINGLE_PRODUCT_F; -typedef typeof(tnxdbl_small_single_product_tmp_bytes) TNXDBL_SMALL_SINGLE_PRODUCT_TMP_BYTES_F; -typedef typeof(znx32_small_single_product) ZNX32_SMALL_SINGLE_PRODUCT_F; -typedef typeof(znx32_small_single_product_tmp_bytes) ZNX32_SMALL_SINGLE_PRODUCT_TMP_BYTES_F; -typedef typeof(tnx32_small_single_product) TNX32_SMALL_SINGLE_PRODUCT_F; -typedef typeof(tnx32_small_single_product_tmp_bytes) TNX32_SMALL_SINGLE_PRODUCT_TMP_BYTES_F; -typedef typeof(rnx_approxdecomp_from_tnx32) RNX_APPROXDECOMP_FROM_TNX32_F; -typedef typeof(rnx_approxdecomp_from_tnx32x2) RNX_APPROXDECOMP_FROM_TNX32X2_F; -typedef typeof(rnx_approxdecomp_from_tnxdbl) RNX_APPROXDECOMP_FROM_TNXDBL_F; -typedef typeof(bytes_of_rnx_svp_ppol) BYTES_OF_RNX_SVP_PPOL_F; -typedef typeof(rnx_svp_prepare) RNX_SVP_PREPARE_F; -typedef typeof(rnx_svp_apply) RNX_SVP_APPLY_F; -typedef typeof(bytes_of_rnx_vmp_pmat) BYTES_OF_RNX_VMP_PMAT_F; -typedef typeof(rnx_vmp_prepare_contiguous) RNX_VMP_PREPARE_CONTIGUOUS_F; -typedef typeof(rnx_vmp_prepare_contiguous_tmp_bytes) RNX_VMP_PREPARE_CONTIGUOUS_TMP_BYTES_F; -typedef typeof(rnx_vmp_apply_tmp_a) RNX_VMP_APPLY_TMP_A_F; -typedef typeof(rnx_vmp_apply_tmp_a_tmp_bytes) RNX_VMP_APPLY_TMP_A_TMP_BYTES_F; -typedef typeof(rnx_vmp_apply_dft_to_dft) RNX_VMP_APPLY_DFT_TO_DFT_F; -typedef typeof(rnx_vmp_apply_dft_to_dft_tmp_bytes) RNX_VMP_APPLY_DFT_TO_DFT_TMP_BYTES_F; -typedef typeof(vec_rnx_dft) VEC_RNX_DFT_F; -typedef typeof(vec_rnx_idft) VEC_RNX_IDFT_F; - -typedef struct rnx_module_vtable_t RNX_MODULE_VTABLE; -struct rnx_module_vtable_t { - VEC_RNX_ZERO_F* vec_rnx_zero; - VEC_RNX_COPY_F* vec_rnx_copy; - VEC_RNX_NEGATE_F* vec_rnx_negate; - VEC_RNX_ADD_F* vec_rnx_add; - VEC_RNX_SUB_F* vec_rnx_sub; - VEC_RNX_ROTATE_F* vec_rnx_rotate; - VEC_RNX_MUL_XP_MINUS_ONE_F* vec_rnx_mul_xp_minus_one; - VEC_RNX_AUTOMORPHISM_F* vec_rnx_automorphism; - VEC_RNX_TO_ZNX32_F* vec_rnx_to_znx32; - VEC_RNX_FROM_ZNX32_F* vec_rnx_from_znx32; - VEC_RNX_TO_TNX32_F* vec_rnx_to_tnx32; - VEC_RNX_FROM_TNX32_F* vec_rnx_from_tnx32; - VEC_RNX_TO_TNX32X2_F* vec_rnx_to_tnx32x2; - VEC_RNX_FROM_TNX32X2_F* vec_rnx_from_tnx32x2; - VEC_RNX_TO_TNXDBL_F* vec_rnx_to_tnxdbl; - RNX_SMALL_SINGLE_PRODUCT_F* rnx_small_single_product; - RNX_SMALL_SINGLE_PRODUCT_TMP_BYTES_F* rnx_small_single_product_tmp_bytes; - TNXDBL_SMALL_SINGLE_PRODUCT_F* tnxdbl_small_single_product; - TNXDBL_SMALL_SINGLE_PRODUCT_TMP_BYTES_F* tnxdbl_small_single_product_tmp_bytes; - ZNX32_SMALL_SINGLE_PRODUCT_F* znx32_small_single_product; - ZNX32_SMALL_SINGLE_PRODUCT_TMP_BYTES_F* znx32_small_single_product_tmp_bytes; - TNX32_SMALL_SINGLE_PRODUCT_F* tnx32_small_single_product; - TNX32_SMALL_SINGLE_PRODUCT_TMP_BYTES_F* tnx32_small_single_product_tmp_bytes; - RNX_APPROXDECOMP_FROM_TNX32_F* rnx_approxdecomp_from_tnx32; - RNX_APPROXDECOMP_FROM_TNX32X2_F* rnx_approxdecomp_from_tnx32x2; - RNX_APPROXDECOMP_FROM_TNXDBL_F* rnx_approxdecomp_from_tnxdbl; - BYTES_OF_RNX_SVP_PPOL_F* bytes_of_rnx_svp_ppol; - RNX_SVP_PREPARE_F* rnx_svp_prepare; - RNX_SVP_APPLY_F* rnx_svp_apply; - BYTES_OF_RNX_VMP_PMAT_F* bytes_of_rnx_vmp_pmat; - RNX_VMP_PREPARE_CONTIGUOUS_F* rnx_vmp_prepare_contiguous; - RNX_VMP_PREPARE_CONTIGUOUS_TMP_BYTES_F* rnx_vmp_prepare_contiguous_tmp_bytes; - RNX_VMP_APPLY_TMP_A_F* rnx_vmp_apply_tmp_a; - RNX_VMP_APPLY_TMP_A_TMP_BYTES_F* rnx_vmp_apply_tmp_a_tmp_bytes; - RNX_VMP_APPLY_DFT_TO_DFT_F* rnx_vmp_apply_dft_to_dft; - RNX_VMP_APPLY_DFT_TO_DFT_TMP_BYTES_F* rnx_vmp_apply_dft_to_dft_tmp_bytes; - VEC_RNX_DFT_F* vec_rnx_dft; - VEC_RNX_IDFT_F* vec_rnx_idft; -}; - -#endif // SPQLIOS_VEC_RNX_ARITHMETIC_PLUGIN_H
D spqlios/lib/spqlios/arithmetic/vec_rnx_arithmetic_private.h

@@ -1,284 +0,0 @@

-#ifndef SPQLIOS_VEC_RNX_ARITHMETIC_PRIVATE_H -#define SPQLIOS_VEC_RNX_ARITHMETIC_PRIVATE_H - -#include "../commons_private.h" -#include "../reim/reim_fft.h" -#include "vec_rnx_arithmetic.h" -#include "vec_rnx_arithmetic_plugin.h" - -typedef struct fft64_rnx_module_precomp_t FFT64_RNX_MODULE_PRECOMP; -struct fft64_rnx_module_precomp_t { - REIM_FFT_PRECOMP* p_fft; - REIM_IFFT_PRECOMP* p_ifft; - REIM_FFTVEC_MUL_PRECOMP* p_fftvec_mul; - REIM_FFTVEC_ADDMUL_PRECOMP* p_fftvec_addmul; -}; - -typedef union rnx_module_precomp_t RNX_MODULE_PRECOMP; -union rnx_module_precomp_t { - FFT64_RNX_MODULE_PRECOMP fft64; -}; - -void fft64_init_rnx_module_precomp(MOD_RNX* module); - -void fft64_finalize_rnx_module_precomp(MOD_RNX* module); - -/** @brief opaque structure that describes the modules (RnX,ZnX,TnX) and the hardware */ -struct rnx_module_info_t { - uint64_t n; - uint64_t m; - RNX_MODULE_TYPE mtype; - RNX_MODULE_VTABLE vtable; - RNX_MODULE_PRECOMP precomp; - void* custom; - void (*custom_deleter)(void*); -}; - -void init_rnx_module_info(MOD_RNX* module, // - uint64_t, RNX_MODULE_TYPE mtype); - -void finalize_rnx_module_info(MOD_RNX* module); - -void fft64_init_rnx_module_vtable(MOD_RNX* module); - -/////////////////////////////////////////////////////////////////// -// prepared gadget decompositions (optimized) // -/////////////////////////////////////////////////////////////////// - -struct tnx32_approxdec_gadget_t { - uint64_t k; - uint64_t ell; - int32_t add_cst; // 1/2.(sum 2^-(i+1)K) - int32_t rshift_base; // 32 - K - int64_t and_mask; // 2^K-1 - int64_t or_mask; // double(2^52) - double sub_cst; // double(2^52 + 2^(K-1)) - uint8_t rshifts[8]; // 32 - (i+1).K -}; - -struct tnx32x2_approxdec_gadget_t { - // TODO -}; - -struct tnxdbl_approxdecomp_gadget_t { - uint64_t k; - uint64_t ell; - double add_cst; // double(3.2^(51-ell.K) + 1/2.(sum 2^(-iK)) for i=[0,ell[) - uint64_t and_mask; // uint64(2^(K)-1) - uint64_t or_mask; // double(2^52) - double sub_cst; // double(2^52 + 2^(K-1)) -}; - -EXPORT void vec_rnx_add_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -); -EXPORT void vec_rnx_add_avx( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -); - -/** @brief sets res = 0 */ -EXPORT void vec_rnx_zero_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl // res -); - -/** @brief sets res = a */ -EXPORT void vec_rnx_copy_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = -a */ -EXPORT void vec_rnx_negate_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = -a */ -EXPORT void vec_rnx_negate_avx( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = a - b */ -EXPORT void vec_rnx_sub_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -); - -/** @brief sets res = a - b */ -EXPORT void vec_rnx_sub_avx( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -); - -/** @brief sets res = a . X^p */ -EXPORT void vec_rnx_rotate_ref( // - const MOD_RNX* module, // N - const int64_t p, // rotation value - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = a(X^p) */ -EXPORT void vec_rnx_automorphism_ref( // - const MOD_RNX* module, // N - int64_t p, // X -> X^p - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief number of bytes in a RNX_VMP_PMAT (for manual allocation) */ -EXPORT uint64_t fft64_bytes_of_rnx_vmp_pmat(const MOD_RNX* module, // N - uint64_t nrows, uint64_t ncols); - -EXPORT void fft64_rnx_vmp_apply_dft_to_dft_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a_dft, uint64_t a_size, uint64_t a_sl, // a - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -); -EXPORT void fft64_rnx_vmp_apply_dft_to_dft_avx( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a_dft, uint64_t a_size, uint64_t a_sl, // a - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -); -EXPORT uint64_t fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_ref( // - const MOD_RNX* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -); -EXPORT uint64_t fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_avx( // - const MOD_RNX* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -); -EXPORT void fft64_rnx_vmp_prepare_contiguous_ref( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double* mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -); -EXPORT void fft64_rnx_vmp_prepare_contiguous_avx( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double* mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -); -EXPORT uint64_t fft64_rnx_vmp_prepare_contiguous_tmp_bytes_ref(const MOD_RNX* module); -EXPORT uint64_t fft64_rnx_vmp_prepare_contiguous_tmp_bytes_avx(const MOD_RNX* module); - -EXPORT void fft64_rnx_vmp_apply_tmp_a_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res (addr must be != a) - double* tmpa, uint64_t a_size, uint64_t a_sl, // a (will be overwritten) - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -); -EXPORT void fft64_rnx_vmp_apply_tmp_a_avx( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res (addr must be != a) - double* tmpa, uint64_t a_size, uint64_t a_sl, // a (will be overwritten) - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -); - -EXPORT uint64_t fft64_rnx_vmp_apply_tmp_a_tmp_bytes_ref( // - const MOD_RNX* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -); -EXPORT uint64_t fft64_rnx_vmp_apply_tmp_a_tmp_bytes_avx( // - const MOD_RNX* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -); - -/// gadget decompositions - -/** @brief sets res = gadget_decompose(a) */ -EXPORT void rnx_approxdecomp_from_tnxdbl_ref( // - const MOD_RNX* module, // N - const TNXDBL_APPROXDECOMP_GADGET* gadget, // output base 2^K - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a); // a -EXPORT void rnx_approxdecomp_from_tnxdbl_avx( // - const MOD_RNX* module, // N - const TNXDBL_APPROXDECOMP_GADGET* gadget, // output base 2^K - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a); // a - -EXPORT void vec_rnx_mul_xp_minus_one_ref( // - const MOD_RNX* module, // N - const int64_t p, // rotation value - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_rnx_to_znx32_ref( // - const MOD_RNX* module, // N - int32_t* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_rnx_from_znx32_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const int32_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_rnx_to_tnx32_ref( // - const MOD_RNX* module, // N - int32_t* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_rnx_from_tnx32_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const int32_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_rnx_to_tnxdbl_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT uint64_t fft64_bytes_of_rnx_svp_ppol(const MOD_RNX* module); // N - -/** @brief prepares a svp polynomial */ -EXPORT void fft64_rnx_svp_prepare_ref(const MOD_RNX* module, // N - RNX_SVP_PPOL* ppol, // output - const double* pol // a -); - -/** @brief apply a svp product, result = ppol * a, presented in DFT space */ -EXPORT void fft64_rnx_svp_apply_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // output - const RNX_SVP_PPOL* ppol, // prepared pol - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -#endif // SPQLIOS_VEC_RNX_ARITHMETIC_PRIVATE_H
D spqlios/lib/spqlios/arithmetic/vec_rnx_conversions_ref.c

@@ -1,91 +0,0 @@

-#include <memory.h> - -#include "vec_rnx_arithmetic_private.h" -#include "zn_arithmetic_private.h" - -EXPORT void vec_rnx_to_znx32_ref( // - const MOD_RNX* module, // N - int32_t* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - dbl_round_to_i32_ref(NULL, res + i * res_sl, nn, a + i * a_sl, nn); - } - for (uint64_t i = msize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(int32_t)); - } -} - -EXPORT void vec_rnx_from_znx32_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const int32_t* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - i32_to_dbl_ref(NULL, res + i * res_sl, nn, a + i * a_sl, nn); - } - for (uint64_t i = msize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(int32_t)); - } -} -EXPORT void vec_rnx_to_tnx32_ref( // - const MOD_RNX* module, // N - int32_t* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - dbl_to_tn32_ref(NULL, res + i * res_sl, nn, a + i * a_sl, nn); - } - for (uint64_t i = msize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(int32_t)); - } -} -EXPORT void vec_rnx_from_tnx32_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const int32_t* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - tn32_to_dbl_ref(NULL, res + i * res_sl, nn, a + i * a_sl, nn); - } - for (uint64_t i = msize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(int32_t)); - } -} - -static void dbl_to_tndbl_ref( // - const void* UNUSED, // N - double* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -) { - static const double OFF_CST = INT64_C(3) << 51; - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - double ai = a[i] + OFF_CST; - res[i] = a[i] - (ai - OFF_CST); - } - memset(res + msize, 0, (res_size - msize) * sizeof(double)); -} - -EXPORT void vec_rnx_to_tnxdbl_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - dbl_to_tndbl_ref(NULL, res + i * res_sl, nn, a + i * a_sl, nn); - } - for (uint64_t i = msize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(int32_t)); - } -}
D spqlios/lib/spqlios/arithmetic/vec_rnx_svp_ref.c

@@ -1,47 +0,0 @@

-#include <string.h> - -#include "../coeffs/coeffs_arithmetic.h" -#include "vec_rnx_arithmetic_private.h" - -EXPORT uint64_t fft64_bytes_of_rnx_svp_ppol(const MOD_RNX* module) { return module->n * sizeof(double); } - -EXPORT RNX_SVP_PPOL* new_rnx_svp_ppol(const MOD_RNX* module) { return spqlios_alloc(bytes_of_rnx_svp_ppol(module)); } - -EXPORT void delete_rnx_svp_ppol(RNX_SVP_PPOL* ppol) { spqlios_free(ppol); } - -/** @brief prepares a svp polynomial */ -EXPORT void fft64_rnx_svp_prepare_ref(const MOD_RNX* module, // N - RNX_SVP_PPOL* ppol, // output - const double* pol // a -) { - double* const dppol = (double*)ppol; - rnx_divide_by_m_ref(module->n, module->m, dppol, pol); - reim_fft(module->precomp.fft64.p_fft, dppol); -} - -EXPORT void fft64_rnx_svp_apply_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // output - const RNX_SVP_PPOL* ppol, // prepared pol - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - double* const dppol = (double*)ppol; - - const uint64_t auto_end_idx = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < auto_end_idx; ++i) { - const double* a_ptr = a + i * a_sl; - double* const res_ptr = res + i * res_sl; - // copy the polynomial to res, apply fft in place, call fftvec - // _mul, apply ifft in place. - memcpy(res_ptr, a_ptr, nn * sizeof(double)); - reim_fft(module->precomp.fft64.p_fft, (double*)res_ptr); - reim_fftvec_mul(module->precomp.fft64.p_fftvec_mul, res_ptr, res_ptr, dppol); - reim_ifft(module->precomp.fft64.p_ifft, res_ptr); - } - - // then extend with zeros - for (uint64_t i = auto_end_idx; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -}
D spqlios/lib/spqlios/arithmetic/vec_rnx_vmp_avx.c

@@ -1,196 +0,0 @@

-#include <assert.h> -#include <immintrin.h> -#include <string.h> - -#include "../coeffs/coeffs_arithmetic.h" -#include "../reim/reim_fft.h" -#include "../reim4/reim4_arithmetic.h" -#include "vec_rnx_arithmetic_private.h" - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void fft64_rnx_vmp_prepare_contiguous_avx( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double* mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -) { - // there is an edge case if nn < 8 - const uint64_t nn = module->n; - const uint64_t m = module->m; - - double* const dtmp = (double*)tmp_space; - double* const output_mat = (double*)pmat; - double* start_addr = (double*)pmat; - uint64_t offset = nrows * ncols * 8; - - if (nn >= 8) { - for (uint64_t row_i = 0; row_i < nrows; row_i++) { - for (uint64_t col_i = 0; col_i < ncols; col_i++) { - rnx_divide_by_m_avx(nn, m, dtmp, mat + (row_i * ncols + col_i) * nn); - reim_fft(module->precomp.fft64.p_fft, dtmp); - - if (col_i == (ncols - 1) && (ncols % 2 == 1)) { - // special case: last column out of an odd column number - start_addr = output_mat + col_i * nrows * 8 // col == ncols-1 - + row_i * 8; - } else { - // general case: columns go by pair - start_addr = output_mat + (col_i / 2) * (2 * nrows) * 8 // second: col pair index - + row_i * 2 * 8 // third: row index - + (col_i % 2) * 8; - } - - for (uint64_t blk_i = 0; blk_i < m / 4; blk_i++) { - // extract blk from tmp and save it - reim4_extract_1blk_from_reim_avx(m, blk_i, start_addr + blk_i * offset, dtmp); - } - } - } - } else { - for (uint64_t row_i = 0; row_i < nrows; row_i++) { - for (uint64_t col_i = 0; col_i < ncols; col_i++) { - double* res = output_mat + (col_i * nrows + row_i) * nn; - rnx_divide_by_m_avx(nn, m, res, mat + (row_i * ncols + col_i) * nn); - reim_fft(module->precomp.fft64.p_fft, res); - } - } - } -} - -/** @brief minimal size of the tmp_space */ -EXPORT void fft64_rnx_vmp_apply_dft_to_dft_avx( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a_dft, uint64_t a_size, uint64_t a_sl, // a - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -) { - const uint64_t m = module->m; - const uint64_t nn = module->n; - - double* mat2cols_output = (double*)tmp_space; // 128 bytes - double* extracted_blk = (double*)tmp_space + 16; // 64*min(nrows,a_size) bytes - - double* mat_input = (double*)pmat; - - const uint64_t row_max = nrows < a_size ? nrows : a_size; - const uint64_t col_max = ncols < res_size ? ncols : res_size; - - if (row_max > 0 && col_max > 0) { - if (nn >= 8) { - // let's do some prefetching of the GSW key, since on some cpus, - // it helps - const uint64_t ms4 = m >> 2; // m/4 - const uint64_t gsw_iter_doubles = 8 * nrows * ncols; - const uint64_t pref_doubles = 1200; - const double* gsw_pref_ptr = mat_input; - const double* const gsw_ptr_end = mat_input + ms4 * gsw_iter_doubles; - const double* gsw_pref_ptr_target = mat_input + pref_doubles; - for (; gsw_pref_ptr < gsw_pref_ptr_target; gsw_pref_ptr += 8) { - __builtin_prefetch(gsw_pref_ptr, 0, _MM_HINT_T0); - } - const double* mat_blk_start; - uint64_t blk_i; - for (blk_i = 0, mat_blk_start = mat_input; blk_i < ms4; blk_i++, mat_blk_start += gsw_iter_doubles) { - // prefetch the next iteration - if (gsw_pref_ptr_target < gsw_ptr_end) { - gsw_pref_ptr_target += gsw_iter_doubles; - if (gsw_pref_ptr_target > gsw_ptr_end) gsw_pref_ptr_target = gsw_ptr_end; - for (; gsw_pref_ptr < gsw_pref_ptr_target; gsw_pref_ptr += 8) { - __builtin_prefetch(gsw_pref_ptr, 0, _MM_HINT_T0); - } - } - reim4_extract_1blk_from_contiguous_reim_sl_avx(m, a_sl, row_max, blk_i, extracted_blk, a_dft); - // apply mat2cols - for (uint64_t col_i = 0; col_i < col_max - 1; col_i += 2) { - uint64_t col_offset = col_i * (8 * nrows); - reim4_vec_mat2cols_product_avx2(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - - reim4_save_1blk_to_reim_avx(m, blk_i, res + col_i * res_sl, mat2cols_output); - reim4_save_1blk_to_reim_avx(m, blk_i, res + (col_i + 1) * res_sl, mat2cols_output + 8); - } - - // check if col_max is odd, then special case - if (col_max % 2 == 1) { - uint64_t last_col = col_max - 1; - uint64_t col_offset = last_col * (8 * nrows); - - // the last column is alone in the pmat: vec_mat1col - if (ncols == col_max) { - reim4_vec_mat1col_product_avx2(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - } else { - // the last column is part of a colpair in the pmat: vec_mat2cols and ignore the second position - reim4_vec_mat2cols_product_avx2(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - } - reim4_save_1blk_to_reim_avx(m, blk_i, res + last_col * res_sl, mat2cols_output); - } - } - } else { - const double* in; - uint64_t in_sl; - if (res == a_dft) { - // it is in place: copy the input vector - in = (double*)tmp_space; - in_sl = nn; - // vec_rnx_copy(module, (double*)tmp_space, row_max, nn, a_dft, row_max, a_sl); - for (uint64_t row_i = 0; row_i < row_max; row_i++) { - memcpy((double*)tmp_space + row_i * nn, a_dft + row_i * a_sl, nn * sizeof(double)); - } - } else { - // it is out of place: do the product directly - in = a_dft; - in_sl = a_sl; - } - for (uint64_t col_i = 0; col_i < col_max; col_i++) { - double* pmat_col = mat_input + col_i * nrows * nn; - { - reim_fftvec_mul(module->precomp.fft64.p_fftvec_mul, // - res + col_i * res_sl, // - in, // - pmat_col); - } - for (uint64_t row_i = 1; row_i < row_max; row_i++) { - reim_fftvec_addmul(module->precomp.fft64.p_fftvec_addmul, // - res + col_i * res_sl, // - in + row_i * in_sl, // - pmat_col + row_i * nn); - } - } - } - } - // zero out remaining bytes (if any) - for (uint64_t i = col_max; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -} - -/** @brief applies a vmp product res = a x pmat */ -EXPORT void fft64_rnx_vmp_apply_tmp_a_avx( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res (addr must be != a) - double* tmpa, uint64_t a_size, uint64_t a_sl, // a (will be overwritten) - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -) { - const uint64_t nn = module->n; - const uint64_t rows = nrows < a_size ? nrows : a_size; - const uint64_t cols = ncols < res_size ? ncols : res_size; - - // fft is done in place on the input (tmpa is destroyed) - for (uint64_t i = 0; i < rows; ++i) { - reim_fft(module->precomp.fft64.p_fft, tmpa + i * a_sl); - } - fft64_rnx_vmp_apply_dft_to_dft_avx(module, // - res, cols, res_sl, // - tmpa, rows, a_sl, // - pmat, nrows, ncols, // - tmp_space); - // ifft is done in place on the output - for (uint64_t i = 0; i < cols; ++i) { - reim_ifft(module->precomp.fft64.p_ifft, res + i * res_sl); - } - // zero out the remaining positions - for (uint64_t i = cols; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -}
D spqlios/lib/spqlios/arithmetic/vec_rnx_vmp_ref.c

@@ -1,251 +0,0 @@

-#include <assert.h> -#include <string.h> - -#include "../coeffs/coeffs_arithmetic.h" -#include "../reim/reim_fft.h" -#include "../reim4/reim4_arithmetic.h" -#include "vec_rnx_arithmetic_private.h" - -/** @brief number of bytes in a RNX_VMP_PMAT (for manual allocation) */ -EXPORT uint64_t fft64_bytes_of_rnx_vmp_pmat(const MOD_RNX* module, // N - uint64_t nrows, uint64_t ncols) { // dimensions - return nrows * ncols * module->n * sizeof(double); -} - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void fft64_rnx_vmp_prepare_contiguous_ref( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double* mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -) { - // there is an edge case if nn < 8 - const uint64_t nn = module->n; - const uint64_t m = module->m; - - double* const dtmp = (double*)tmp_space; - double* const output_mat = (double*)pmat; - double* start_addr = (double*)pmat; - uint64_t offset = nrows * ncols * 8; - - if (nn >= 8) { - for (uint64_t row_i = 0; row_i < nrows; row_i++) { - for (uint64_t col_i = 0; col_i < ncols; col_i++) { - rnx_divide_by_m_ref(nn, m, dtmp, mat + (row_i * ncols + col_i) * nn); - reim_fft(module->precomp.fft64.p_fft, dtmp); - - if (col_i == (ncols - 1) && (ncols % 2 == 1)) { - // special case: last column out of an odd column number - start_addr = output_mat + col_i * nrows * 8 // col == ncols-1 - + row_i * 8; - } else { - // general case: columns go by pair - start_addr = output_mat + (col_i / 2) * (2 * nrows) * 8 // second: col pair index - + row_i * 2 * 8 // third: row index - + (col_i % 2) * 8; - } - - for (uint64_t blk_i = 0; blk_i < m / 4; blk_i++) { - // extract blk from tmp and save it - reim4_extract_1blk_from_reim_ref(m, blk_i, start_addr + blk_i * offset, dtmp); - } - } - } - } else { - for (uint64_t row_i = 0; row_i < nrows; row_i++) { - for (uint64_t col_i = 0; col_i < ncols; col_i++) { - double* res = output_mat + (col_i * nrows + row_i) * nn; - rnx_divide_by_m_ref(nn, m, res, mat + (row_i * ncols + col_i) * nn); - reim_fft(module->precomp.fft64.p_fft, res); - } - } - } -} - -/** @brief number of scratch bytes necessary to prepare a matrix */ -EXPORT uint64_t fft64_rnx_vmp_prepare_contiguous_tmp_bytes_ref(const MOD_RNX* module) { - const uint64_t nn = module->n; - return nn * sizeof(int64_t); -} - -/** @brief minimal size of the tmp_space */ -EXPORT void fft64_rnx_vmp_apply_dft_to_dft_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a_dft, uint64_t a_size, uint64_t a_sl, // a - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -) { - const uint64_t m = module->m; - const uint64_t nn = module->n; - - double* mat2cols_output = (double*)tmp_space; // 128 bytes - double* extracted_blk = (double*)tmp_space + 16; // 64*min(nrows,a_size) bytes - - double* mat_input = (double*)pmat; - - const uint64_t row_max = nrows < a_size ? nrows : a_size; - const uint64_t col_max = ncols < res_size ? ncols : res_size; - - if (row_max > 0 && col_max > 0) { - if (nn >= 8) { - for (uint64_t blk_i = 0; blk_i < m / 4; blk_i++) { - double* mat_blk_start = mat_input + blk_i * (8 * nrows * ncols); - - reim4_extract_1blk_from_contiguous_reim_sl_ref(m, a_sl, row_max, blk_i, extracted_blk, a_dft); - // apply mat2cols - for (uint64_t col_i = 0; col_i < col_max - 1; col_i += 2) { - uint64_t col_offset = col_i * (8 * nrows); - reim4_vec_mat2cols_product_ref(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - - reim4_save_1blk_to_reim_ref(m, blk_i, res + col_i * res_sl, mat2cols_output); - reim4_save_1blk_to_reim_ref(m, blk_i, res + (col_i + 1) * res_sl, mat2cols_output + 8); - } - - // check if col_max is odd, then special case - if (col_max % 2 == 1) { - uint64_t last_col = col_max - 1; - uint64_t col_offset = last_col * (8 * nrows); - - // the last column is alone in the pmat: vec_mat1col - if (ncols == col_max) { - reim4_vec_mat1col_product_ref(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - } else { - // the last column is part of a colpair in the pmat: vec_mat2cols and ignore the second position - reim4_vec_mat2cols_product_ref(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - } - reim4_save_1blk_to_reim_ref(m, blk_i, res + last_col * res_sl, mat2cols_output); - } - } - } else { - const double* in; - uint64_t in_sl; - if (res == a_dft) { - // it is in place: copy the input vector - in = (double*)tmp_space; - in_sl = nn; - // vec_rnx_copy(module, (double*)tmp_space, row_max, nn, a_dft, row_max, a_sl); - for (uint64_t row_i = 0; row_i < row_max; row_i++) { - memcpy((double*)tmp_space + row_i * nn, a_dft + row_i * a_sl, nn * sizeof(double)); - } - } else { - // it is out of place: do the product directly - in = a_dft; - in_sl = a_sl; - } - for (uint64_t col_i = 0; col_i < col_max; col_i++) { - double* pmat_col = mat_input + col_i * nrows * nn; - { - reim_fftvec_mul(module->precomp.fft64.p_fftvec_mul, // - res + col_i * res_sl, // - in, // - pmat_col); - } - for (uint64_t row_i = 1; row_i < row_max; row_i++) { - reim_fftvec_addmul(module->precomp.fft64.p_fftvec_addmul, // - res + col_i * res_sl, // - in + row_i * in_sl, // - pmat_col + row_i * nn); - } - } - } - } - // zero out remaining bytes (if any) - for (uint64_t i = col_max; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -} - -/** @brief applies a vmp product res = a x pmat */ -EXPORT void fft64_rnx_vmp_apply_tmp_a_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res (addr must be != a) - double* tmpa, uint64_t a_size, uint64_t a_sl, // a (will be overwritten) - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -) { - const uint64_t nn = module->n; - const uint64_t rows = nrows < a_size ? nrows : a_size; - const uint64_t cols = ncols < res_size ? ncols : res_size; - - // fft is done in place on the input (tmpa is destroyed) - for (uint64_t i = 0; i < rows; ++i) { - reim_fft(module->precomp.fft64.p_fft, tmpa + i * a_sl); - } - fft64_rnx_vmp_apply_dft_to_dft_ref(module, // - res, cols, res_sl, // - tmpa, rows, a_sl, // - pmat, nrows, ncols, // - tmp_space); - // ifft is done in place on the output - for (uint64_t i = 0; i < cols; ++i) { - reim_ifft(module->precomp.fft64.p_ifft, res + i * res_sl); - } - // zero out the remaining positions - for (uint64_t i = cols; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -} - -/** @brief minimal size of the tmp_space */ -EXPORT uint64_t fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_ref( // - const MOD_RNX* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -) { - const uint64_t row_max = nrows < a_size ? nrows : a_size; - - return (128) + (64 * row_max); -} - -#ifdef __APPLE__ -EXPORT uint64_t fft64_rnx_vmp_apply_tmp_a_tmp_bytes_ref( // - const MOD_RNX* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -) { - return fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_ref(module, res_size, a_size, nrows, ncols); -} -#else -EXPORT uint64_t fft64_rnx_vmp_apply_tmp_a_tmp_bytes_ref( // - const MOD_RNX* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix - ) __attribute((alias("fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_ref"))); -#endif -// avx aliases that need to be defined in the same .c file - -/** @brief number of scratch bytes necessary to prepare a matrix */ -#ifdef __APPLE__ -#pragma weak fft64_rnx_vmp_prepare_contiguous_tmp_bytes_avx = fft64_rnx_vmp_prepare_contiguous_tmp_bytes_ref -#else -EXPORT uint64_t fft64_rnx_vmp_prepare_contiguous_tmp_bytes_avx(const MOD_RNX* module) - __attribute((alias("fft64_rnx_vmp_prepare_contiguous_tmp_bytes_ref"))); -#endif - -/** @brief minimal size of the tmp_space */ -#ifdef __APPLE__ -#pragma weak fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_avx = fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_ref -#else -EXPORT uint64_t fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_avx( // - const MOD_RNX* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix - ) __attribute((alias("fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_ref"))); -#endif - -#ifdef __APPLE__ -#pragma weak fft64_rnx_vmp_apply_tmp_a_tmp_bytes_avx = fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_ref -#else -EXPORT uint64_t fft64_rnx_vmp_apply_tmp_a_tmp_bytes_avx( // - const MOD_RNX* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix - ) __attribute((alias("fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_ref"))); -#endif -// wrappers
D spqlios/lib/spqlios/arithmetic/vec_znx.c

@@ -1,333 +0,0 @@

-#include <assert.h> -#include <math.h> -#include <stdint.h> -#include <stdlib.h> -#include <string.h> - -#include "../coeffs/coeffs_arithmetic.h" -#include "../q120/q120_arithmetic.h" -#include "../q120/q120_ntt.h" -#include "../reim/reim_fft_internal.h" -#include "../reim4/reim4_arithmetic.h" -#include "vec_znx_arithmetic.h" -#include "vec_znx_arithmetic_private.h" - -// general function (virtual dispatch) - -EXPORT void vec_znx_add(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - module->func.vec_znx_add(module, // N - res, res_size, res_sl, // res - a, a_size, a_sl, // a - b, b_size, b_sl // b - ); -} - -EXPORT void vec_znx_sub(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - module->func.vec_znx_sub(module, // N - res, res_size, res_sl, // res - a, a_size, a_sl, // a - b, b_size, b_sl // b - ); -} - -EXPORT void vec_znx_rotate(const MODULE* module, // N - const int64_t p, // rotation value - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - module->func.vec_znx_rotate(module, // N - p, // p - res, res_size, res_sl, // res - a, a_size, a_sl // a - ); -} - -EXPORT void vec_znx_automorphism(const MODULE* module, // N - const int64_t p, // X->X^p - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - module->func.vec_znx_automorphism(module, // N - p, // p - res, res_size, res_sl, // res - a, a_size, a_sl // a - ); -} - -EXPORT void vec_znx_normalize_base2k(const MODULE* module, // N - uint64_t log2_base2k, // output base 2^K - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - uint8_t* tmp_space // scratch space of size >= N -) { - module->func.vec_znx_normalize_base2k(module, // N - log2_base2k, // log2_base2k - res, res_size, res_sl, // res - a, a_size, a_sl, // a - tmp_space); -} - -EXPORT uint64_t vec_znx_normalize_base2k_tmp_bytes(const MODULE* module // N -) { - return module->func.vec_znx_normalize_base2k_tmp_bytes(module // N - ); -} - -// specialized function (ref) - -EXPORT void vec_znx_add_ref(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t nn = module->nn; - if (a_size <= b_size) { - const uint64_t sum_idx = res_size < a_size ? res_size : a_size; - const uint64_t copy_idx = res_size < b_size ? res_size : b_size; - // add up to the smallest dimension - for (uint64_t i = 0; i < sum_idx; ++i) { - znx_add_i64_ref(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - // then copy to the largest dimension - for (uint64_t i = sum_idx; i < copy_idx; ++i) { - znx_copy_i64_ref(nn, res + i * res_sl, b + i * b_sl); - } - // then extend with zeros - for (uint64_t i = copy_idx; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } - } else { - const uint64_t sum_idx = res_size < b_size ? res_size : b_size; - const uint64_t copy_idx = res_size < a_size ? res_size : a_size; - // add up to the smallest dimension - for (uint64_t i = 0; i < sum_idx; ++i) { - znx_add_i64_ref(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - // then copy to the largest dimension - for (uint64_t i = sum_idx; i < copy_idx; ++i) { - znx_copy_i64_ref(nn, res + i * res_sl, a + i * a_sl); - } - // then extend with zeros - for (uint64_t i = copy_idx; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } - } -} - -EXPORT void vec_znx_sub_ref(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t nn = module->nn; - if (a_size <= b_size) { - const uint64_t sub_idx = res_size < a_size ? res_size : a_size; - const uint64_t copy_idx = res_size < b_size ? res_size : b_size; - // subtract up to the smallest dimension - for (uint64_t i = 0; i < sub_idx; ++i) { - znx_sub_i64_ref(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - // then negate to the largest dimension - for (uint64_t i = sub_idx; i < copy_idx; ++i) { - znx_negate_i64_ref(nn, res + i * res_sl, b + i * b_sl); - } - // then extend with zeros - for (uint64_t i = copy_idx; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } - } else { - const uint64_t sub_idx = res_size < b_size ? res_size : b_size; - const uint64_t copy_idx = res_size < a_size ? res_size : a_size; - // subtract up to the smallest dimension - for (uint64_t i = 0; i < sub_idx; ++i) { - znx_sub_i64_ref(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - // then copy to the largest dimension - for (uint64_t i = sub_idx; i < copy_idx; ++i) { - znx_copy_i64_ref(nn, res + i * res_sl, a + i * a_sl); - } - // then extend with zeros - for (uint64_t i = copy_idx; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } - } -} - -EXPORT void vec_znx_rotate_ref(const MODULE* module, // N - const int64_t p, // rotation value - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->nn; - - const uint64_t rot_end_idx = res_size < a_size ? res_size : a_size; - // rotate up to the smallest dimension - for (uint64_t i = 0; i < rot_end_idx; ++i) { - int64_t* res_ptr = res + i * res_sl; - const int64_t* a_ptr = a + i * a_sl; - if (res_ptr == a_ptr) { - znx_rotate_inplace_i64(nn, p, res_ptr); - } else { - znx_rotate_i64(nn, p, res_ptr, a_ptr); - } - } - // then extend with zeros - for (uint64_t i = rot_end_idx; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } -} - -EXPORT void vec_znx_automorphism_ref(const MODULE* module, // N - const int64_t p, // X->X^p - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->nn; - - const uint64_t auto_end_idx = res_size < a_size ? res_size : a_size; - - for (uint64_t i = 0; i < auto_end_idx; ++i) { - int64_t* res_ptr = res + i * res_sl; - const int64_t* a_ptr = a + i * a_sl; - if (res_ptr == a_ptr) { - znx_automorphism_inplace_i64(nn, p, res_ptr); - } else { - znx_automorphism_i64(nn, p, res_ptr, a_ptr); - } - } - // then extend with zeros - for (uint64_t i = auto_end_idx; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } -} - -EXPORT void vec_znx_normalize_base2k_ref(const MODULE* module, // N - uint64_t log2_base2k, // output base 2^K - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - uint8_t* tmp_space // scratch space of size >= N -) { - const uint64_t nn = module->nn; - - // use MSB limb of res for carry propagation - int64_t* cout = (int64_t*)tmp_space; - int64_t* cin = 0x0; - - // propagate carry until first limb of res - int64_t i = a_size - 1; - for (; i >= res_size; --i) { - znx_normalize(nn, log2_base2k, 0x0, cout, a + i * a_sl, cin); - cin = cout; - } - - // propagate carry and normalize - for (; i >= 1; --i) { - znx_normalize(nn, log2_base2k, res + i * res_sl, cout, a + i * a_sl, cin); - cin = cout; - } - - // normalize last limb - znx_normalize(nn, log2_base2k, res, 0x0, a, cin); - - // extend result with zeros - for (uint64_t i = a_size; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } -} - -EXPORT uint64_t vec_znx_normalize_base2k_tmp_bytes_ref(const MODULE* module // N -) { - const uint64_t nn = module->nn; - return nn * sizeof(int64_t); -} - - -// alias have to be defined in this unit: do not move -#ifdef __APPLE__ -EXPORT uint64_t fft64_vec_znx_big_range_normalize_base2k_tmp_bytes( // - const MODULE* module // N - ) { - return vec_znx_normalize_base2k_tmp_bytes_ref(module); -} -EXPORT uint64_t fft64_vec_znx_big_normalize_base2k_tmp_bytes( // - const MODULE* module // N -) { - return vec_znx_normalize_base2k_tmp_bytes_ref(module); -} -#else -EXPORT uint64_t fft64_vec_znx_big_normalize_base2k_tmp_bytes( // - const MODULE* module // N -) __attribute((alias("vec_znx_normalize_base2k_tmp_bytes_ref"))); - -EXPORT uint64_t fft64_vec_znx_big_range_normalize_base2k_tmp_bytes( // - const MODULE* module // N -) __attribute((alias("vec_znx_normalize_base2k_tmp_bytes_ref"))); -#endif - -/** @brief sets res = 0 */ -EXPORT void vec_znx_zero(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl // res -) { - module->func.vec_znx_zero(module, res, res_size, res_sl); -} - -/** @brief sets res = a */ -EXPORT void vec_znx_copy(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - module->func.vec_znx_copy(module, res, res_size, res_sl, a, a_size, a_sl); -} - -/** @brief sets res = a */ -EXPORT void vec_znx_negate(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - module->func.vec_znx_negate(module, res, res_size, res_sl, a, a_size, a_sl); -} - -EXPORT void vec_znx_zero_ref(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl // res -) { - uint64_t nn = module->nn; - for (uint64_t i = 0; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } -} - -EXPORT void vec_znx_copy_ref(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - uint64_t nn = module->nn; - uint64_t smin = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < smin; ++i) { - znx_copy_i64_ref(nn, res + i * res_sl, a + i * a_sl); - } - for (uint64_t i = smin; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } -} - -EXPORT void vec_znx_negate_ref(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - uint64_t nn = module->nn; - uint64_t smin = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < smin; ++i) { - znx_negate_i64_ref(nn, res + i * res_sl, a + i * a_sl); - } - for (uint64_t i = smin; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } -}
D spqlios/lib/spqlios/arithmetic/vec_znx_arithmetic.h

@@ -1,357 +0,0 @@

-#ifndef SPQLIOS_VEC_ZNX_ARITHMETIC_H -#define SPQLIOS_VEC_ZNX_ARITHMETIC_H - -#include <stdint.h> - -#include "../commons.h" -#include "../reim/reim_fft.h" - -/** - * We support the following module families: - * - FFT64: - * all the polynomials should fit at all times over 52 bits. - * for FHE implementations, the recommended limb-sizes are - * between K=10 and 20, which is good for low multiplicative depths. - * - NTT120: - * all the polynomials should fit at all times over 119 bits. - * for FHE implementations, the recommended limb-sizes are - * between K=20 and 40, which is good for large multiplicative depths. - */ -typedef enum module_type_t { FFT64, NTT120 } MODULE_TYPE; - -/** @brief opaque structure that describr the modules (ZnX,TnX) and the hardware */ -typedef struct module_info_t MODULE; -/** @brief opaque type that represents a prepared matrix */ -typedef struct vmp_pmat_t VMP_PMAT; -/** @brief opaque type that represents a vector of znx in DFT space */ -typedef struct vec_znx_dft_t VEC_ZNX_DFT; -/** @brief opaque type that represents a vector of znx in large coeffs space */ -typedef struct vec_znx_bigcoeff_t VEC_ZNX_BIG; -/** @brief opaque type that represents a prepared scalar vector product */ -typedef struct svp_ppol_t SVP_PPOL; -/** @brief opaque type that represents a prepared left convolution vector product */ -typedef struct cnv_pvec_l_t CNV_PVEC_L; -/** @brief opaque type that represents a prepared right convolution vector product */ -typedef struct cnv_pvec_r_t CNV_PVEC_R; - -/** @brief bytes needed for a vec_znx in DFT space */ -EXPORT uint64_t bytes_of_vec_znx_dft(const MODULE* module, // N - uint64_t size); - -/** @brief allocates a vec_znx in DFT space */ -EXPORT VEC_ZNX_DFT* new_vec_znx_dft(const MODULE* module, // N - uint64_t size); - -/** @brief frees memory from a vec_znx in DFT space */ -EXPORT void delete_vec_znx_dft(VEC_ZNX_DFT* res); - -/** @brief bytes needed for a vec_znx_big */ -EXPORT uint64_t bytes_of_vec_znx_big(const MODULE* module, // N - uint64_t size); - -/** @brief allocates a vec_znx_big */ -EXPORT VEC_ZNX_BIG* new_vec_znx_big(const MODULE* module, // N - uint64_t size); -/** @brief frees memory from a vec_znx_big */ -EXPORT void delete_vec_znx_big(VEC_ZNX_BIG* res); - -/** @brief bytes needed for a prepared vector */ -EXPORT uint64_t bytes_of_svp_ppol(const MODULE* module); // N - -/** @brief allocates a prepared vector */ -EXPORT SVP_PPOL* new_svp_ppol(const MODULE* module); // N - -/** @brief frees memory for a prepared vector */ -EXPORT void delete_svp_ppol(SVP_PPOL* res); - -/** @brief bytes needed for a prepared matrix */ -EXPORT uint64_t bytes_of_vmp_pmat(const MODULE* module, // N - uint64_t nrows, uint64_t ncols); - -/** @brief allocates a prepared matrix */ -EXPORT VMP_PMAT* new_vmp_pmat(const MODULE* module, // N - uint64_t nrows, uint64_t ncols); - -/** @brief frees memory for a prepared matrix */ -EXPORT void delete_vmp_pmat(VMP_PMAT* res); - -/** - * @brief obtain a module info for ring dimension N - * the module-info knows about: - * - the dimension N (or the complex dimension m=N/2) - * - any moduleuted fft or ntt items - * - the hardware (avx, arm64, x86, ...) - */ -EXPORT MODULE* new_module_info(uint64_t N, MODULE_TYPE mode); -EXPORT void delete_module_info(MODULE* module_info); -EXPORT uint64_t module_get_n(const MODULE* module); - -/** @brief sets res = 0 */ -EXPORT void vec_znx_zero(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl // res -); - -/** @brief sets res = a */ -EXPORT void vec_znx_copy(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = a */ -EXPORT void vec_znx_negate(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = a + b */ -EXPORT void vec_znx_add(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); - -/** @brief sets res = a - b */ -EXPORT void vec_znx_sub(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); - -/** @brief sets res = k-normalize-reduce(a) */ -EXPORT void vec_znx_normalize_base2k(const MODULE* module, // N - uint64_t log2_base2k, // output base 2^K - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - uint8_t* tmp_space // scratch space (size >= N) -); - -/** @brief returns the minimal byte length of scratch space for vec_znx_normalize_base2k */ -EXPORT uint64_t vec_znx_normalize_base2k_tmp_bytes(const MODULE* module // N -); - -/** @brief sets res = a . X^p */ -EXPORT void vec_znx_rotate(const MODULE* module, // N - const int64_t p, // rotation value - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = a(X^p) */ -EXPORT void vec_znx_automorphism(const MODULE* module, // N - const int64_t p, // X-X^p - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void vmp_prepare_contiguous(const MODULE* module, // N - VMP_PMAT* pmat, // output - const int64_t* mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -); - -/** @brief prepares a vmp matrix (mat[row*ncols+col] points to the item) */ -EXPORT void vmp_prepare_dblptr(const MODULE* module, // N - VMP_PMAT* pmat, // output - const int64_t** mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -); - -/** @brief sets res = 0 */ -EXPORT void vec_dft_zero(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size // res -); - -/** @brief sets res = a+b */ -EXPORT void vec_dft_add(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a, uint64_t a_size, // a - const VEC_ZNX_DFT* b, uint64_t b_size // b -); - -/** @brief sets res = a-b */ -EXPORT void vec_dft_sub(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a, uint64_t a_size, // a - const VEC_ZNX_DFT* b, uint64_t b_size // b -); - -/** @brief sets res = DFT(a) */ -EXPORT void vec_znx_dft(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = iDFT(a_dft) -- output in big coeffs space */ -EXPORT void vec_znx_idft(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - uint8_t* tmp // scratch space -); - -/** @brief tmp bytes required for vec_znx_idft */ -EXPORT uint64_t vec_znx_idft_tmp_bytes(const MODULE* module); - -/** - * @brief sets res = iDFT(a_dft) -- output in big coeffs space - * - * @note a_dft is overwritten - */ -EXPORT void vec_znx_idft_tmp_a(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - VEC_ZNX_DFT* a_dft, uint64_t a_size // a is overwritten -); - -/** @brief sets res = a+b */ -EXPORT void vec_znx_big_add(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -); -/** @brief sets res = a+b */ -EXPORT void vec_znx_big_add_small(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); -EXPORT void vec_znx_big_add_small2(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); - -/** @brief sets res = a-b */ -EXPORT void vec_znx_big_sub(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -); -EXPORT void vec_znx_big_sub_small_b(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); -EXPORT void vec_znx_big_sub_small_a(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -); -EXPORT void vec_znx_big_sub_small2(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); - -/** @brief sets res = k-normalize(a) -- output in int64 coeffs space */ -EXPORT void vec_znx_big_normalize_base2k(const MODULE* module, // N - uint64_t log2_base2k, // base-2^k - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - uint8_t* tmp_space // temp space -); - -/** @brief returns the minimal byte length of scratch space for vec_znx_big_normalize_base2k */ -EXPORT uint64_t vec_znx_big_normalize_base2k_tmp_bytes(const MODULE* module // N -); - -/** @brief apply a svp product, result = ppol * a, presented in DFT space */ -EXPORT void fft64_svp_apply_dft(const MODULE* module, // N - const VEC_ZNX_DFT* res, uint64_t res_size, // output - const SVP_PPOL* ppol, // prepared pol - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = k-normalize(a.subrange) -- output in int64 coeffs space */ -EXPORT void vec_znx_big_range_normalize_base2k( // - const MODULE* module, // N - uint64_t log2_base2k, // base-2^k - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const VEC_ZNX_BIG* a, uint64_t a_range_begin, uint64_t a_range_xend, uint64_t a_range_step, // range - uint8_t* tmp_space // temp space -); - -/** @brief returns the minimal byte length of scratch space for vec_znx_big_range_normalize_base2k */ -EXPORT uint64_t vec_znx_big_range_normalize_base2k_tmp_bytes( // - const MODULE* module // N -); - -/** @brief sets res = a . X^p */ -EXPORT void vec_znx_big_rotate(const MODULE* module, // N - int64_t p, // rotation value - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size // a -); - -/** @brief sets res = a(X^p) */ -EXPORT void vec_znx_big_automorphism(const MODULE* module, // N - int64_t p, // X-X^p - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size // a -); - -/** @brief apply a svp product, result = ppol * a, presented in DFT space */ -EXPORT void svp_apply_dft(const MODULE* module, // N - const VEC_ZNX_DFT* res, uint64_t res_size, // output - const SVP_PPOL* ppol, // prepared pol - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief prepares a svp polynomial */ -EXPORT void svp_prepare(const MODULE* module, // N - SVP_PPOL* ppol, // output - const int64_t* pol // a -); - -/** @brief res = a * b : small integer polynomial product */ -EXPORT void znx_small_single_product(const MODULE* module, // N - int64_t* res, // output - const int64_t* a, // a - const int64_t* b, // b - uint8_t* tmp); - -/** @brief tmp bytes required for znx_small_single_product */ -EXPORT uint64_t znx_small_single_product_tmp_bytes(const MODULE* module); - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void vmp_prepare_contiguous(const MODULE* module, // N - VMP_PMAT* pmat, // output - const int64_t* mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -); - -/** @brief minimal scratch space byte-size required for the vmp_prepare function */ -EXPORT uint64_t vmp_prepare_contiguous_tmp_bytes(const MODULE* module, // N - uint64_t nrows, uint64_t ncols); - -/** @brief applies a vmp product (result in DFT space) */ -EXPORT void vmp_apply_dft(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -); - -/** @brief minimal size of the tmp_space */ -EXPORT uint64_t vmp_apply_dft_tmp_bytes(const MODULE* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -); - -/** @brief minimal size of the tmp_space */ -EXPORT void vmp_apply_dft_to_dft(const MODULE* module, // N - VEC_ZNX_DFT* res, const uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - const VMP_PMAT* pmat, const uint64_t nrows, - const uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -); -; - -/** @brief minimal size of the tmp_space */ -EXPORT uint64_t vmp_apply_dft_to_dft_tmp_bytes(const MODULE* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -); -#endif // SPQLIOS_VEC_ZNX_ARITHMETIC_H
D spqlios/lib/spqlios/arithmetic/vec_znx_arithmetic_private.h

@@ -1,481 +0,0 @@

-#ifndef SPQLIOS_VEC_ZNX_ARITHMETIC_PRIVATE_H -#define SPQLIOS_VEC_ZNX_ARITHMETIC_PRIVATE_H - -#include "../commons_private.h" -#include "../q120/q120_ntt.h" -#include "vec_znx_arithmetic.h" - -/** - * Layouts families: - * - * fft64: - * K: <= 20, N: <= 65536, ell: <= 200 - * vec<ZnX> normalized: represented by int64 - * vec<ZnX> large: represented by int64 (expect <=52 bits) - * vec<ZnX> DFT: represented by double (reim_fft space) - * On AVX2 inftastructure, PMAT, LCNV, RCNV use a special reim4_fft space - * - * ntt120: - * K: <= 50, N: <= 65536, ell: <= 80 - * vec<ZnX> normalized: represented by int64 - * vec<ZnX> large: represented by int128 (expect <=120 bits) - * vec<ZnX> DFT: represented by int64x4 (ntt120 space) - * On AVX2 inftastructure, PMAT, LCNV, RCNV use a special ntt120 space - * - * ntt104: - * K: <= 40, N: <= 65536, ell: <= 80 - * vec<ZnX> normalized: represented by int64 - * vec<ZnX> large: represented by int128 (expect <=120 bits) - * vec<ZnX> DFT: represented by int64x4 (ntt120 space) - * On AVX512 inftastructure, PMAT, LCNV, RCNV use a special ntt104 space - */ - -struct fft64_module_info_t { - // pre-computation for reim_fft - REIM_FFT_PRECOMP* p_fft; - // pre-computation for mul_fft - REIM_FFTVEC_MUL_PRECOMP* mul_fft; - // pre-computation for reim_from_znx6 - REIM_FROM_ZNX64_PRECOMP* p_conv; - // pre-computation for reim_tp_znx6 - REIM_TO_ZNX64_PRECOMP* p_reim_to_znx; - // pre-computation for reim_fft - REIM_IFFT_PRECOMP* p_ifft; - // pre-computation for reim_fftvec_addmul - REIM_FFTVEC_ADDMUL_PRECOMP* p_addmul; -}; - -struct q120_module_info_t { - // pre-computation for q120b to q120b ntt - q120_ntt_precomp* p_ntt; - // pre-computation for q120b to q120b intt - q120_ntt_precomp* p_intt; -}; - -// TODO add function types here -typedef typeof(vec_znx_zero) VEC_ZNX_ZERO_F; -typedef typeof(vec_znx_copy) VEC_ZNX_COPY_F; -typedef typeof(vec_znx_negate) VEC_ZNX_NEGATE_F; -typedef typeof(vec_znx_add) VEC_ZNX_ADD_F; -typedef typeof(vec_znx_dft) VEC_ZNX_DFT_F; -typedef typeof(vec_znx_idft) VEC_ZNX_IDFT_F; -typedef typeof(vec_znx_idft_tmp_bytes) VEC_ZNX_IDFT_TMP_BYTES_F; -typedef typeof(vec_znx_idft_tmp_a) VEC_ZNX_IDFT_TMP_A_F; -typedef typeof(vec_znx_sub) VEC_ZNX_SUB_F; -typedef typeof(vec_znx_rotate) VEC_ZNX_ROTATE_F; -typedef typeof(vec_znx_automorphism) VEC_ZNX_AUTOMORPHISM_F; -typedef typeof(vec_znx_normalize_base2k) VEC_ZNX_NORMALIZE_BASE2K_F; -typedef typeof(vec_znx_normalize_base2k_tmp_bytes) VEC_ZNX_NORMALIZE_BASE2K_TMP_BYTES_F; -typedef typeof(vec_znx_big_normalize_base2k) VEC_ZNX_BIG_NORMALIZE_BASE2K_F; -typedef typeof(vec_znx_big_normalize_base2k_tmp_bytes) VEC_ZNX_BIG_NORMALIZE_BASE2K_TMP_BYTES_F; -typedef typeof(vec_znx_big_range_normalize_base2k) VEC_ZNX_BIG_RANGE_NORMALIZE_BASE2K_F; -typedef typeof(vec_znx_big_range_normalize_base2k_tmp_bytes) VEC_ZNX_BIG_RANGE_NORMALIZE_BASE2K_TMP_BYTES_F; -typedef typeof(vec_znx_big_add) VEC_ZNX_BIG_ADD_F; -typedef typeof(vec_znx_big_add_small) VEC_ZNX_BIG_ADD_SMALL_F; -typedef typeof(vec_znx_big_add_small2) VEC_ZNX_BIG_ADD_SMALL2_F; -typedef typeof(vec_znx_big_sub) VEC_ZNX_BIG_SUB_F; -typedef typeof(vec_znx_big_sub_small_a) VEC_ZNX_BIG_SUB_SMALL_A_F; -typedef typeof(vec_znx_big_sub_small_b) VEC_ZNX_BIG_SUB_SMALL_B_F; -typedef typeof(vec_znx_big_sub_small2) VEC_ZNX_BIG_SUB_SMALL2_F; -typedef typeof(vec_znx_big_rotate) VEC_ZNX_BIG_ROTATE_F; -typedef typeof(vec_znx_big_automorphism) VEC_ZNX_BIG_AUTOMORPHISM_F; -typedef typeof(svp_prepare) SVP_PREPARE; -typedef typeof(svp_apply_dft) SVP_APPLY_DFT_F; -typedef typeof(znx_small_single_product) ZNX_SMALL_SINGLE_PRODUCT_F; -typedef typeof(znx_small_single_product_tmp_bytes) ZNX_SMALL_SINGLE_PRODUCT_TMP_BYTES_F; -typedef typeof(vmp_prepare_contiguous) VMP_PREPARE_CONTIGUOUS_F; -typedef typeof(vmp_prepare_contiguous_tmp_bytes) VMP_PREPARE_CONTIGUOUS_TMP_BYTES_F; -typedef typeof(vmp_apply_dft) VMP_APPLY_DFT_F; -typedef typeof(vmp_apply_dft_tmp_bytes) VMP_APPLY_DFT_TMP_BYTES_F; -typedef typeof(vmp_apply_dft_to_dft) VMP_APPLY_DFT_TO_DFT_F; -typedef typeof(vmp_apply_dft_to_dft_tmp_bytes) VMP_APPLY_DFT_TO_DFT_TMP_BYTES_F; -typedef typeof(bytes_of_vec_znx_dft) BYTES_OF_VEC_ZNX_DFT_F; -typedef typeof(bytes_of_vec_znx_big) BYTES_OF_VEC_ZNX_BIG_F; -typedef typeof(bytes_of_svp_ppol) BYTES_OF_SVP_PPOL_F; -typedef typeof(bytes_of_vmp_pmat) BYTES_OF_VMP_PMAT_F; - -struct module_virtual_functions_t { - // TODO add functions here - VEC_ZNX_ZERO_F* vec_znx_zero; - VEC_ZNX_COPY_F* vec_znx_copy; - VEC_ZNX_NEGATE_F* vec_znx_negate; - VEC_ZNX_ADD_F* vec_znx_add; - VEC_ZNX_DFT_F* vec_znx_dft; - VEC_ZNX_IDFT_F* vec_znx_idft; - VEC_ZNX_IDFT_TMP_BYTES_F* vec_znx_idft_tmp_bytes; - VEC_ZNX_IDFT_TMP_A_F* vec_znx_idft_tmp_a; - VEC_ZNX_SUB_F* vec_znx_sub; - VEC_ZNX_ROTATE_F* vec_znx_rotate; - VEC_ZNX_AUTOMORPHISM_F* vec_znx_automorphism; - VEC_ZNX_NORMALIZE_BASE2K_F* vec_znx_normalize_base2k; - VEC_ZNX_NORMALIZE_BASE2K_TMP_BYTES_F* vec_znx_normalize_base2k_tmp_bytes; - VEC_ZNX_BIG_NORMALIZE_BASE2K_F* vec_znx_big_normalize_base2k; - VEC_ZNX_BIG_NORMALIZE_BASE2K_TMP_BYTES_F* vec_znx_big_normalize_base2k_tmp_bytes; - VEC_ZNX_BIG_RANGE_NORMALIZE_BASE2K_F* vec_znx_big_range_normalize_base2k; - VEC_ZNX_BIG_RANGE_NORMALIZE_BASE2K_TMP_BYTES_F* vec_znx_big_range_normalize_base2k_tmp_bytes; - VEC_ZNX_BIG_ADD_F* vec_znx_big_add; - VEC_ZNX_BIG_ADD_SMALL_F* vec_znx_big_add_small; - VEC_ZNX_BIG_ADD_SMALL2_F* vec_znx_big_add_small2; - VEC_ZNX_BIG_SUB_F* vec_znx_big_sub; - VEC_ZNX_BIG_SUB_SMALL_A_F* vec_znx_big_sub_small_a; - VEC_ZNX_BIG_SUB_SMALL_B_F* vec_znx_big_sub_small_b; - VEC_ZNX_BIG_SUB_SMALL2_F* vec_znx_big_sub_small2; - VEC_ZNX_BIG_ROTATE_F* vec_znx_big_rotate; - VEC_ZNX_BIG_AUTOMORPHISM_F* vec_znx_big_automorphism; - SVP_PREPARE* svp_prepare; - SVP_APPLY_DFT_F* svp_apply_dft; - ZNX_SMALL_SINGLE_PRODUCT_F* znx_small_single_product; - ZNX_SMALL_SINGLE_PRODUCT_TMP_BYTES_F* znx_small_single_product_tmp_bytes; - VMP_PREPARE_CONTIGUOUS_F* vmp_prepare_contiguous; - VMP_PREPARE_CONTIGUOUS_TMP_BYTES_F* vmp_prepare_contiguous_tmp_bytes; - VMP_APPLY_DFT_F* vmp_apply_dft; - VMP_APPLY_DFT_TMP_BYTES_F* vmp_apply_dft_tmp_bytes; - VMP_APPLY_DFT_TO_DFT_F* vmp_apply_dft_to_dft; - VMP_APPLY_DFT_TO_DFT_TMP_BYTES_F* vmp_apply_dft_to_dft_tmp_bytes; - BYTES_OF_VEC_ZNX_DFT_F* bytes_of_vec_znx_dft; - BYTES_OF_VEC_ZNX_BIG_F* bytes_of_vec_znx_big; - BYTES_OF_SVP_PPOL_F* bytes_of_svp_ppol; - BYTES_OF_VMP_PMAT_F* bytes_of_vmp_pmat; -}; - -union backend_module_info_t { - struct fft64_module_info_t fft64; - struct q120_module_info_t q120; -}; - -struct module_info_t { - // generic parameters - MODULE_TYPE module_type; - uint64_t nn; - uint64_t m; - // backend_dependent functions - union backend_module_info_t mod; - // virtual functions - struct module_virtual_functions_t func; -}; - -EXPORT uint64_t fft64_bytes_of_vec_znx_dft(const MODULE* module, // N - uint64_t size); - -EXPORT uint64_t fft64_bytes_of_vec_znx_big(const MODULE* module, // N - uint64_t size); - -EXPORT uint64_t fft64_bytes_of_svp_ppol(const MODULE* module); // N - -EXPORT uint64_t fft64_bytes_of_vmp_pmat(const MODULE* module, // N - uint64_t nrows, uint64_t ncols); - -EXPORT void vec_znx_zero_ref(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl // res -); - -EXPORT void vec_znx_copy_ref(const MODULE* precomp, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_znx_negate_ref(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_znx_negate_avx(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_znx_add_ref(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); -EXPORT void vec_znx_add_avx(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); - -EXPORT void vec_znx_sub_ref(const MODULE* precomp, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); - -EXPORT void vec_znx_sub_avx(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); - -EXPORT void vec_znx_normalize_base2k_ref(const MODULE* module, // N - uint64_t log2_base2k, // output base 2^K - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // inp - uint8_t* tmp_space // scratch space -); - -EXPORT uint64_t vec_znx_normalize_base2k_tmp_bytes_ref(const MODULE* module // N -); - -EXPORT void vec_znx_rotate_ref(const MODULE* module, // N - const int64_t p, // rotation value - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_znx_automorphism_ref(const MODULE* module, // N - const int64_t p, // X->X^p - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vmp_prepare_ref(const MODULE* precomp, // N - VMP_PMAT* pmat, // output - const int64_t* mat, uint64_t nrows, uint64_t ncols // a -); - -EXPORT void vmp_apply_dft_ref(const MODULE* precomp, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols // prep matrix -); - -EXPORT void vec_dft_zero_ref(const MODULE* precomp, // N - VEC_ZNX_DFT* res, uint64_t res_size // res -); - -EXPORT void vec_dft_add_ref(const MODULE* precomp, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a, uint64_t a_size, // a - const VEC_ZNX_DFT* b, uint64_t b_size // b -); - -EXPORT void vec_dft_sub_ref(const MODULE* precomp, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a, uint64_t a_size, // a - const VEC_ZNX_DFT* b, uint64_t b_size // b -); - -EXPORT void vec_dft_ref(const MODULE* precomp, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_idft_ref(const MODULE* precomp, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size); - -EXPORT void vec_znx_big_normalize_ref(const MODULE* precomp, // N - uint64_t k, // base-2^k - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const VEC_ZNX_BIG* a, uint64_t a_size // a -); - -/** @brief apply a svp product, result = ppol * a, presented in DFT space */ -EXPORT void fft64_svp_apply_dft_ref(const MODULE* module, // N - const VEC_ZNX_DFT* res, uint64_t res_size, // output - const SVP_PPOL* ppol, // prepared pol - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = k-normalize(a) -- output in int64 coeffs space */ -EXPORT void fft64_vec_znx_big_normalize_base2k(const MODULE* module, // N - uint64_t k, // base-2^k - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - uint8_t* tmp_space // temp space -); - -/** @brief returns the minimal byte length of scratch space for vec_znx_big_normalize_base2k */ -EXPORT uint64_t fft64_vec_znx_big_normalize_base2k_tmp_bytes(const MODULE* module // N - -); - -/** @brief sets res = k-normalize(a.subrange) -- output in int64 coeffs space */ -EXPORT void fft64_vec_znx_big_range_normalize_base2k(const MODULE* module, // N - uint64_t log2_base2k, // base-2^k - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const VEC_ZNX_BIG* a, uint64_t a_range_begin, // a - uint64_t a_range_xend, uint64_t a_range_step, // range - uint8_t* tmp_space // temp space -); - -/** @brief returns the minimal byte length of scratch space for vec_znx_big_range_normalize_base2k */ -EXPORT uint64_t fft64_vec_znx_big_range_normalize_base2k_tmp_bytes(const MODULE* module // N -); - -EXPORT void fft64_vec_znx_dft(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void fft64_vec_znx_idft(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - uint8_t* tmp // scratch space -); - -EXPORT uint64_t fft64_vec_znx_idft_tmp_bytes(const MODULE* module); - -EXPORT void fft64_vec_znx_idft_tmp_a(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - VEC_ZNX_DFT* a_dft, uint64_t a_size // a is overwritten -); - -EXPORT void ntt120_vec_znx_dft_avx(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -/** */ -EXPORT void ntt120_vec_znx_idft_avx(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - uint8_t* tmp // scratch space -); - -EXPORT uint64_t ntt120_vec_znx_idft_tmp_bytes_avx(const MODULE* module); - -EXPORT void ntt120_vec_znx_idft_tmp_a_avx(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - VEC_ZNX_DFT* a_dft, uint64_t a_size // a is overwritten -); - -// big additions/subtractions - -/** @brief sets res = a+b */ -EXPORT void fft64_vec_znx_big_add(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -); -/** @brief sets res = a+b */ -EXPORT void fft64_vec_znx_big_add_small(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); -EXPORT void fft64_vec_znx_big_add_small2(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); - -/** @brief sets res = a-b */ -EXPORT void fft64_vec_znx_big_sub(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -); -EXPORT void fft64_vec_znx_big_sub_small_b(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); -EXPORT void fft64_vec_znx_big_sub_small_a(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -); -EXPORT void fft64_vec_znx_big_sub_small2(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); - -/** @brief sets res = a . X^p */ -EXPORT void fft64_vec_znx_big_rotate(const MODULE* module, // N - int64_t p, // rotation value - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size // a -); - -/** @brief sets res = a(X^p) */ -EXPORT void fft64_vec_znx_big_automorphism(const MODULE* module, // N - int64_t p, // X-X^p - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size // a -); - -/** @brief prepares a svp polynomial */ -EXPORT void fft64_svp_prepare_ref(const MODULE* module, // N - SVP_PPOL* ppol, // output - const int64_t* pol // a -); - -/** @brief res = a * b : small integer polynomial product */ -EXPORT void fft64_znx_small_single_product(const MODULE* module, // N - int64_t* res, // output - const int64_t* a, // a - const int64_t* b, // b - uint8_t* tmp); - -/** @brief tmp bytes required for znx_small_single_product */ -EXPORT uint64_t fft64_znx_small_single_product_tmp_bytes(const MODULE* module); - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void fft64_vmp_prepare_contiguous_ref(const MODULE* module, // N - VMP_PMAT* pmat, // output - const int64_t* mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -); - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void fft64_vmp_prepare_contiguous_avx(const MODULE* module, // N - VMP_PMAT* pmat, // output - const int64_t* mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -); - -/** @brief minimal scratch space byte-size required for the vmp_prepare function */ -EXPORT uint64_t fft64_vmp_prepare_contiguous_tmp_bytes(const MODULE* module, // N - uint64_t nrows, uint64_t ncols); - -/** @brief applies a vmp product (result in DFT space) */ -EXPORT void fft64_vmp_apply_dft_ref(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -); - -/** @brief applies a vmp product (result in DFT space) */ -EXPORT void fft64_vmp_apply_dft_avx(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -); - -/** @brief this inner function could be very handy */ -EXPORT void fft64_vmp_apply_dft_to_dft_ref(const MODULE* module, // N - VEC_ZNX_DFT* res, const uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - const VMP_PMAT* pmat, const uint64_t nrows, - const uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -); - -/** @brief this inner function could be very handy */ -EXPORT void fft64_vmp_apply_dft_to_dft_avx(const MODULE* module, // N - VEC_ZNX_DFT* res, const uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - const VMP_PMAT* pmat, const uint64_t nrows, - const uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -); - -/** @brief minimal size of the tmp_space */ -EXPORT uint64_t fft64_vmp_apply_dft_tmp_bytes(const MODULE* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -); - -/** @brief minimal size of the tmp_space */ -EXPORT uint64_t fft64_vmp_apply_dft_to_dft_tmp_bytes(const MODULE* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -); -#endif // SPQLIOS_VEC_ZNX_ARITHMETIC_PRIVATE_H
D spqlios/lib/spqlios/arithmetic/vec_znx_avx.c

@@ -1,103 +0,0 @@

-#include <string.h> - -#include "../coeffs/coeffs_arithmetic.h" -#include "../reim4/reim4_arithmetic.h" -#include "vec_znx_arithmetic_private.h" - -// specialized function (ref) - -// Note: these functions do not have an avx variant. -#define znx_copy_i64_avx znx_copy_i64_ref -#define znx_zero_i64_avx znx_zero_i64_ref - -EXPORT void vec_znx_add_avx(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t nn = module->nn; - if (a_size <= b_size) { - const uint64_t sum_idx = res_size < a_size ? res_size : a_size; - const uint64_t copy_idx = res_size < b_size ? res_size : b_size; - // add up to the smallest dimension - for (uint64_t i = 0; i < sum_idx; ++i) { - znx_add_i64_avx(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - // then copy to the largest dimension - for (uint64_t i = sum_idx; i < copy_idx; ++i) { - znx_copy_i64_avx(nn, res + i * res_sl, b + i * b_sl); - } - // then extend with zeros - for (uint64_t i = copy_idx; i < res_size; ++i) { - znx_zero_i64_avx(nn, res + i * res_sl); - } - } else { - const uint64_t sum_idx = res_size < b_size ? res_size : b_size; - const uint64_t copy_idx = res_size < a_size ? res_size : a_size; - // add up to the smallest dimension - for (uint64_t i = 0; i < sum_idx; ++i) { - znx_add_i64_avx(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - // then copy to the largest dimension - for (uint64_t i = sum_idx; i < copy_idx; ++i) { - znx_copy_i64_avx(nn, res + i * res_sl, a + i * a_sl); - } - // then extend with zeros - for (uint64_t i = copy_idx; i < res_size; ++i) { - znx_zero_i64_avx(nn, res + i * res_sl); - } - } -} - -EXPORT void vec_znx_sub_avx(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t nn = module->nn; - if (a_size <= b_size) { - const uint64_t sub_idx = res_size < a_size ? res_size : a_size; - const uint64_t copy_idx = res_size < b_size ? res_size : b_size; - // subtract up to the smallest dimension - for (uint64_t i = 0; i < sub_idx; ++i) { - znx_sub_i64_avx(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - // then negate to the largest dimension - for (uint64_t i = sub_idx; i < copy_idx; ++i) { - znx_negate_i64_avx(nn, res + i * res_sl, b + i * b_sl); - } - // then extend with zeros - for (uint64_t i = copy_idx; i < res_size; ++i) { - znx_zero_i64_avx(nn, res + i * res_sl); - } - } else { - const uint64_t sub_idx = res_size < b_size ? res_size : b_size; - const uint64_t copy_idx = res_size < a_size ? res_size : a_size; - // subtract up to the smallest dimension - for (uint64_t i = 0; i < sub_idx; ++i) { - znx_sub_i64_avx(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - // then copy to the largest dimension - for (uint64_t i = sub_idx; i < copy_idx; ++i) { - znx_copy_i64_avx(nn, res + i * res_sl, a + i * a_sl); - } - // then extend with zeros - for (uint64_t i = copy_idx; i < res_size; ++i) { - znx_zero_i64_avx(nn, res + i * res_sl); - } - } -} - -EXPORT void vec_znx_negate_avx(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - uint64_t nn = module->nn; - uint64_t smin = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < smin; ++i) { - znx_negate_i64_avx(nn, res + i * res_sl, a + i * a_sl); - } - for (uint64_t i = smin; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } -}
D spqlios/lib/spqlios/arithmetic/vec_znx_big.c

@@ -1,270 +0,0 @@

-#include "vec_znx_arithmetic_private.h" - -EXPORT uint64_t bytes_of_vec_znx_big(const MODULE* module, // N - uint64_t size) { - return module->func.bytes_of_vec_znx_big(module, size); -} - -// public wrappers - -/** @brief sets res = a+b */ -EXPORT void vec_znx_big_add(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -) { - module->func.vec_znx_big_add(module, res, res_size, a, a_size, b, b_size); -} - -/** @brief sets res = a+b */ -EXPORT void vec_znx_big_add_small(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - module->func.vec_znx_big_add_small(module, res, res_size, a, a_size, b, b_size, b_sl); -} - -EXPORT void vec_znx_big_add_small2(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - module->func.vec_znx_big_add_small2(module, res, res_size, a, a_size, a_sl, b, b_size, b_sl); -} - -/** @brief sets res = a-b */ -EXPORT void vec_znx_big_sub(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -) { - module->func.vec_znx_big_sub(module, res, res_size, a, a_size, b, b_size); -} - -EXPORT void vec_znx_big_sub_small_b(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - module->func.vec_znx_big_sub_small_b(module, res, res_size, a, a_size, b, b_size, b_sl); -} - -EXPORT void vec_znx_big_sub_small_a(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -) { - module->func.vec_znx_big_sub_small_a(module, res, res_size, a, a_size, a_sl, b, b_size); -} -EXPORT void vec_znx_big_sub_small2(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - module->func.vec_znx_big_sub_small2(module, res, res_size, a, a_size, a_sl, b, b_size, b_sl); -} - -/** @brief sets res = a . X^p */ -EXPORT void vec_znx_big_rotate(const MODULE* module, // N - int64_t p, // rotation value - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size // a -) { - module->func.vec_znx_big_rotate(module, p, res, res_size, a, a_size); -} - -/** @brief sets res = a(X^p) */ -EXPORT void vec_znx_big_automorphism(const MODULE* module, // N - int64_t p, // X-X^p - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size // a -) { - module->func.vec_znx_big_automorphism(module, p, res, res_size, a, a_size); -} - -// private wrappers - -EXPORT uint64_t fft64_bytes_of_vec_znx_big(const MODULE* module, // N - uint64_t size) { - return module->nn * size * sizeof(double); -} - -EXPORT VEC_ZNX_BIG* new_vec_znx_big(const MODULE* module, // N - uint64_t size) { - return spqlios_alloc(bytes_of_vec_znx_big(module, size)); -} - -EXPORT void delete_vec_znx_big(VEC_ZNX_BIG* res) { spqlios_free(res); } - -/** @brief sets res = a+b */ -EXPORT void fft64_vec_znx_big_add(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -) { - const uint64_t n = module->nn; - vec_znx_add(module, // - (int64_t*)res, res_size, n, // - (int64_t*)a, a_size, n, // - (int64_t*)b, b_size, n); -} -/** @brief sets res = a+b */ -EXPORT void fft64_vec_znx_big_add_small(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t n = module->nn; - vec_znx_add(module, // - (int64_t*)res, res_size, n, // - (int64_t*)a, a_size, n, // - b, b_size, b_sl); -} -EXPORT void fft64_vec_znx_big_add_small2(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t n = module->nn; - vec_znx_add(module, // - (int64_t*)res, res_size, n, // - a, a_size, a_sl, // - b, b_size, b_sl); -} - -/** @brief sets res = a-b */ -EXPORT void fft64_vec_znx_big_sub(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -) { - const uint64_t n = module->nn; - vec_znx_sub(module, // - (int64_t*)res, res_size, n, // - (int64_t*)a, a_size, n, // - (int64_t*)b, b_size, n); -} - -EXPORT void fft64_vec_znx_big_sub_small_b(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t n = module->nn; - vec_znx_sub(module, // - (int64_t*)res, res_size, n, // - (int64_t*)a, a_size, // - n, b, b_size, b_sl); -} -EXPORT void fft64_vec_znx_big_sub_small_a(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -) { - const uint64_t n = module->nn; - vec_znx_sub(module, // - (int64_t*)res, res_size, n, // - a, a_size, a_sl, // - (int64_t*)b, b_size, n); -} -EXPORT void fft64_vec_znx_big_sub_small2(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t n = module->nn; - vec_znx_sub(module, // - (int64_t*)res, res_size, // - n, a, a_size, // - a_sl, b, b_size, b_sl); -} - -/** @brief sets res = a . X^p */ -EXPORT void fft64_vec_znx_big_rotate(const MODULE* module, // N - int64_t p, // rotation value - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size // a -) { - uint64_t nn = module->nn; - vec_znx_rotate(module, p, (int64_t*)res, res_size, nn, (int64_t*)a, a_size, nn); -} - -/** @brief sets res = a(X^p) */ -EXPORT void fft64_vec_znx_big_automorphism(const MODULE* module, // N - int64_t p, // X-X^p - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size // a -) { - uint64_t nn = module->nn; - vec_znx_automorphism(module, p, (int64_t*)res, res_size, nn, (int64_t*)a, a_size, nn); -} - -EXPORT void vec_znx_big_normalize_base2k(const MODULE* module, // N - uint64_t k, // base-2^k - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - uint8_t* tmp_space // temp space -) { - module->func.vec_znx_big_normalize_base2k(module, // N - k, // base-2^k - res, res_size, res_sl, // res - a, a_size, // a - tmp_space); -} - -EXPORT uint64_t vec_znx_big_normalize_base2k_tmp_bytes(const MODULE* module // N -) { - return module->func.vec_znx_big_normalize_base2k_tmp_bytes(module // N - ); -} - -/** @brief sets res = k-normalize(a.subrange) -- output in int64 coeffs space */ -EXPORT void vec_znx_big_range_normalize_base2k( // - const MODULE* module, // N - uint64_t log2_base2k, // base-2^k - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const VEC_ZNX_BIG* a, uint64_t a_range_begin, uint64_t a_range_xend, uint64_t a_range_step, // range - uint8_t* tmp_space // temp space -) { - module->func.vec_znx_big_range_normalize_base2k(module, log2_base2k, res, res_size, res_sl, a, a_range_begin, - a_range_xend, a_range_step, tmp_space); -} - -/** @brief returns the minimal byte length of scratch space for vec_znx_big_range_normalize_base2k */ -EXPORT uint64_t vec_znx_big_range_normalize_base2k_tmp_bytes( // - const MODULE* module // N -) { - return module->func.vec_znx_big_range_normalize_base2k_tmp_bytes(module); -} - -EXPORT void fft64_vec_znx_big_normalize_base2k(const MODULE* module, // N - uint64_t k, // base-2^k - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - uint8_t* tmp_space) { - uint64_t a_sl = module->nn; - module->func.vec_znx_normalize_base2k(module, // N - k, // log2_base2k - res, res_size, res_sl, // res - (int64_t*)a, a_size, a_sl, // a - tmp_space); -} - -EXPORT void fft64_vec_znx_big_range_normalize_base2k( // - const MODULE* module, // N - uint64_t k, // base-2^k - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const VEC_ZNX_BIG* a, uint64_t a_begin, uint64_t a_end, uint64_t a_step, // a - uint8_t* tmp_space) { - // convert the range indexes to int64[] slices - const int64_t* a_st = ((int64_t*)a) + module->nn * a_begin; - const uint64_t a_size = (a_end + a_step - 1 - a_begin) / a_step; - const uint64_t a_sl = module->nn * a_step; - // forward the call - module->func.vec_znx_normalize_base2k(module, // N - k, // log2_base2k - res, res_size, res_sl, // res - a_st, a_size, a_sl, // a - tmp_space); -}
D spqlios/lib/spqlios/arithmetic/vec_znx_dft.c

@@ -1,162 +0,0 @@

-#include <string.h> - -#include "../q120/q120_arithmetic.h" -#include "vec_znx_arithmetic_private.h" - -EXPORT void vec_znx_dft(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - return module->func.vec_znx_dft(module, res, res_size, a, a_size, a_sl); -} - -EXPORT void vec_znx_idft(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - uint8_t* tmp // scratch space -) { - return module->func.vec_znx_idft(module, res, res_size, a_dft, a_size, tmp); -} - -EXPORT uint64_t vec_znx_idft_tmp_bytes(const MODULE* module) { return module->func.vec_znx_idft_tmp_bytes(module); } - -EXPORT void vec_znx_idft_tmp_a(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - VEC_ZNX_DFT* a_dft, uint64_t a_size // a is overwritten -) { - return module->func.vec_znx_idft_tmp_a(module, res, res_size, a_dft, a_size); -} - -EXPORT uint64_t bytes_of_vec_znx_dft(const MODULE* module, // N - uint64_t size) { - return module->func.bytes_of_vec_znx_dft(module, size); -} - -// fft64 backend -EXPORT uint64_t fft64_bytes_of_vec_znx_dft(const MODULE* module, // N - uint64_t size) { - return module->nn * size * sizeof(double); -} - -EXPORT VEC_ZNX_DFT* new_vec_znx_dft(const MODULE* module, // N - uint64_t size) { - return spqlios_alloc(bytes_of_vec_znx_dft(module, size)); -} - -EXPORT void delete_vec_znx_dft(VEC_ZNX_DFT* res) { spqlios_free(res); } - -EXPORT void fft64_vec_znx_dft(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t smin = res_size < a_size ? res_size : a_size; - const uint64_t nn = module->nn; - - for (uint64_t i = 0; i < smin; i++) { - reim_from_znx64(module->mod.fft64.p_conv, ((double*)res) + i * nn, a + i * a_sl); - reim_fft(module->mod.fft64.p_fft, ((double*)res) + i * nn); - } - - // fill up remaining part with 0's - double* const dres = (double*)res; - memset(dres + smin * nn, 0, (res_size - smin) * nn * sizeof(double)); -} - -EXPORT void fft64_vec_znx_idft(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - uint8_t* tmp // unused -) { - const uint64_t nn = module->nn; - const uint64_t smin = res_size < a_size ? res_size : a_size; - if ((double*)res != (double*)a_dft) { - memcpy(res, a_dft, smin * nn * sizeof(double)); - } - - for (uint64_t i = 0; i < smin; i++) { - reim_ifft(module->mod.fft64.p_ifft, ((double*)res) + i * nn); - reim_to_znx64(module->mod.fft64.p_reim_to_znx, ((int64_t*)res) + i * nn, ((int64_t*)res) + i * nn); - } - - // fill up remaining part with 0's - int64_t* const dres = (int64_t*)res; - memset(dres + smin * nn, 0, (res_size - smin) * nn * sizeof(double)); -} - -EXPORT uint64_t fft64_vec_znx_idft_tmp_bytes(const MODULE* module) { return 0; } - -EXPORT void fft64_vec_znx_idft_tmp_a(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - VEC_ZNX_DFT* a_dft, uint64_t a_size // a is overwritten -) { - const uint64_t nn = module->nn; - const uint64_t smin = res_size < a_size ? res_size : a_size; - - int64_t* const tres = (int64_t*)res; - double* const ta = (double*)a_dft; - for (uint64_t i = 0; i < smin; i++) { - reim_ifft(module->mod.fft64.p_ifft, ta + i * nn); - reim_to_znx64(module->mod.fft64.p_reim_to_znx, tres + i * nn, ta + i * nn); - } - - // fill up remaining part with 0's - memset(tres + smin * nn, 0, (res_size - smin) * nn * sizeof(double)); -} - -// ntt120 backend - -EXPORT void ntt120_vec_znx_dft_avx(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->nn; - const uint64_t smin = res_size < a_size ? res_size : a_size; - - int64_t* tres = (int64_t*)res; - for (uint64_t i = 0; i < smin; i++) { - q120_b_from_znx64_simple(nn, (q120b*)(tres + i * nn * 4), a + i * a_sl); - q120_ntt_bb_avx2(module->mod.q120.p_ntt, (q120b*)(tres + i * nn * 4)); - } - - // fill up remaining part with 0's - memset(tres + smin * nn * 4, 0, (res_size - smin) * nn * 4 * sizeof(int64_t)); -} - -EXPORT void ntt120_vec_znx_idft_avx(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - uint8_t* tmp) { - const uint64_t nn = module->nn; - const uint64_t smin = res_size < a_size ? res_size : a_size; - - __int128_t* const tres = (__int128_t*)res; - const int64_t* const ta = (int64_t*)a_dft; - for (uint64_t i = 0; i < smin; i++) { - memcpy(tmp, ta + i * nn * 4, nn * 4 * sizeof(uint64_t)); - q120_intt_bb_avx2(module->mod.q120.p_intt, (q120b*)tmp); - q120_b_to_znx128_simple(nn, tres + i * nn, (q120b*)tmp); - } - - // fill up remaining part with 0's - memset(tres + smin * nn, 0, (res_size - smin) * nn * sizeof(*tres)); -} - -EXPORT uint64_t ntt120_vec_znx_idft_tmp_bytes_avx(const MODULE* module) { return module->nn * 4 * sizeof(uint64_t); } - -EXPORT void ntt120_vec_znx_idft_tmp_a_avx(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - VEC_ZNX_DFT* a_dft, uint64_t a_size // a is overwritten -) { - const uint64_t nn = module->nn; - const uint64_t smin = res_size < a_size ? res_size : a_size; - - __int128_t* const tres = (__int128_t*)res; - int64_t* const ta = (int64_t*)a_dft; - for (uint64_t i = 0; i < smin; i++) { - q120_intt_bb_avx2(module->mod.q120.p_intt, (q120b*)(ta + i * nn * 4)); - q120_b_to_znx128_simple(nn, tres + i * nn, (q120b*)(ta + i * nn * 4)); - } - - // fill up remaining part with 0's - memset(tres + smin * nn, 0, (res_size - smin) * nn * sizeof(*tres)); -}
D spqlios/lib/spqlios/arithmetic/vec_znx_dft_avx2.c

@@ -1,1 +0,0 @@

-#include "vec_znx_arithmetic_private.h"
D spqlios/lib/spqlios/arithmetic/vector_matrix_product.c

@@ -1,240 +0,0 @@

-#include <string.h> - -#include "../reim4/reim4_arithmetic.h" -#include "vec_znx_arithmetic_private.h" - -EXPORT uint64_t bytes_of_vmp_pmat(const MODULE* module, // N - uint64_t nrows, uint64_t ncols // dimensions -) { - return module->func.bytes_of_vmp_pmat(module, nrows, ncols); -} - -// fft64 -EXPORT uint64_t fft64_bytes_of_vmp_pmat(const MODULE* module, // N - uint64_t nrows, uint64_t ncols // dimensions -) { - return module->nn * nrows * ncols * sizeof(double); -} - -EXPORT VMP_PMAT* new_vmp_pmat(const MODULE* module, // N - uint64_t nrows, uint64_t ncols // dimensions -) { - return spqlios_alloc(bytes_of_vmp_pmat(module, nrows, ncols)); -} - -EXPORT void delete_vmp_pmat(VMP_PMAT* res) { spqlios_free(res); } - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void vmp_prepare_contiguous(const MODULE* module, // N - VMP_PMAT* pmat, // output - const int64_t* mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -) { - module->func.vmp_prepare_contiguous(module, pmat, mat, nrows, ncols, tmp_space); -} - -/** @brief minimal scratch space byte-size required for the vmp_prepare function */ -EXPORT uint64_t vmp_prepare_contiguous_tmp_bytes(const MODULE* module, // N - uint64_t nrows, uint64_t ncols) { - return module->func.vmp_prepare_contiguous_tmp_bytes(module, nrows, ncols); -} - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void fft64_vmp_prepare_contiguous_ref(const MODULE* module, // N - VMP_PMAT* pmat, // output - const int64_t* mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -) { - // there is an edge case if nn < 8 - const uint64_t nn = module->nn; - const uint64_t m = module->m; - - double* output_mat = (double*)pmat; - double* start_addr = (double*)pmat; - uint64_t offset = nrows * ncols * 8; - - if (nn >= 8) { - for (uint64_t row_i = 0; row_i < nrows; row_i++) { - for (uint64_t col_i = 0; col_i < ncols; col_i++) { - reim_from_znx64(module->mod.fft64.p_conv, (SVP_PPOL*)tmp_space, mat + (row_i * ncols + col_i) * nn); - reim_fft(module->mod.fft64.p_fft, (double*)tmp_space); - - if (col_i == (ncols - 1) && (ncols % 2 == 1)) { - // special case: last column out of an odd column number - start_addr = output_mat + col_i * nrows * 8 // col == ncols-1 - + row_i * 8; - } else { - // general case: columns go by pair - start_addr = output_mat + (col_i / 2) * (2 * nrows) * 8 // second: col pair index - + row_i * 2 * 8 // third: row index - + (col_i % 2) * 8; - } - - for (uint64_t blk_i = 0; blk_i < m / 4; blk_i++) { - // extract blk from tmp and save it - reim4_extract_1blk_from_reim_ref(m, blk_i, start_addr + blk_i * offset, (double*)tmp_space); - } - } - } - } else { - for (uint64_t row_i = 0; row_i < nrows; row_i++) { - for (uint64_t col_i = 0; col_i < ncols; col_i++) { - double* res = (double*)pmat + (col_i * nrows + row_i) * nn; - reim_from_znx64(module->mod.fft64.p_conv, (SVP_PPOL*)res, mat + (row_i * ncols + col_i) * nn); - reim_fft(module->mod.fft64.p_fft, res); - } - } - } -} - -/** @brief minimal scratch space byte-size required for the vmp_prepare function */ -EXPORT uint64_t fft64_vmp_prepare_contiguous_tmp_bytes(const MODULE* module, // N - uint64_t nrows, uint64_t ncols) { - const uint64_t nn = module->nn; - return nn * sizeof(int64_t); -} - -/** @brief applies a vmp product (result in DFT space) */ -EXPORT void fft64_vmp_apply_dft_ref(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -) { - const uint64_t nn = module->nn; - const uint64_t rows = nrows < a_size ? nrows : a_size; - - VEC_ZNX_DFT* a_dft = (VEC_ZNX_DFT*)tmp_space; - uint8_t* new_tmp_space = (uint8_t*)tmp_space + rows * nn * sizeof(double); - - fft64_vec_znx_dft(module, a_dft, rows, a, a_size, a_sl); - fft64_vmp_apply_dft_to_dft_ref(module, res, res_size, a_dft, a_size, pmat, nrows, ncols, new_tmp_space); -} - -/** @brief this inner function could be very handy */ -EXPORT void fft64_vmp_apply_dft_to_dft_ref(const MODULE* module, // N - VEC_ZNX_DFT* res, const uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - const VMP_PMAT* pmat, const uint64_t nrows, - const uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -) { - const uint64_t m = module->m; - const uint64_t nn = module->nn; - - double* mat2cols_output = (double*)tmp_space; // 128 bytes - double* extracted_blk = (double*)tmp_space + 16; // 64*min(nrows,a_size) bytes - - double* mat_input = (double*)pmat; - double* vec_input = (double*)a_dft; - double* vec_output = (double*)res; - - const uint64_t row_max = nrows < a_size ? nrows : a_size; - const uint64_t col_max = ncols < res_size ? ncols : res_size; - - if (nn >= 8) { - for (uint64_t blk_i = 0; blk_i < m / 4; blk_i++) { - double* mat_blk_start = mat_input + blk_i * (8 * nrows * ncols); - - reim4_extract_1blk_from_contiguous_reim_ref(m, row_max, blk_i, (double*)extracted_blk, (double*)a_dft); - // apply mat2cols - for (uint64_t col_i = 0; col_i < col_max - 1; col_i += 2) { - uint64_t col_offset = col_i * (8 * nrows); - reim4_vec_mat2cols_product_ref(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - - reim4_save_1blk_to_reim_ref(m, blk_i, vec_output + col_i * nn, mat2cols_output); - reim4_save_1blk_to_reim_ref(m, blk_i, vec_output + (col_i + 1) * nn, mat2cols_output + 8); - } - - // check if col_max is odd, then special case - if (col_max % 2 == 1) { - uint64_t last_col = col_max - 1; - uint64_t col_offset = last_col * (8 * nrows); - - // the last column is alone in the pmat: vec_mat1col - if (ncols == col_max) { - reim4_vec_mat1col_product_ref(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - } else { - // the last column is part of a colpair in the pmat: vec_mat2cols and ignore the second position - reim4_vec_mat2cols_product_ref(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - } - reim4_save_1blk_to_reim_ref(m, blk_i, vec_output + last_col * nn, mat2cols_output); - } - } - } else { - for (uint64_t col_i = 0; col_i < col_max; col_i++) { - double* pmat_col = mat_input + col_i * nrows * nn; - for (uint64_t row_i = 0; row_i < 1; row_i++) { - reim_fftvec_mul(module->mod.fft64.mul_fft, vec_output + col_i * nn, vec_input + row_i * nn, - pmat_col + row_i * nn); - } - for (uint64_t row_i = 1; row_i < row_max; row_i++) { - reim_fftvec_addmul(module->mod.fft64.p_addmul, vec_output + col_i * nn, vec_input + row_i * nn, - pmat_col + row_i * nn); - } - } - } - - // zero out remaining bytes - memset(vec_output + col_max * nn, 0, (res_size - col_max) * nn * sizeof(double)); -} - -/** @brief minimal size of the tmp_space */ -EXPORT uint64_t fft64_vmp_apply_dft_tmp_bytes(const MODULE* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -) { - const uint64_t nn = module->nn; - const uint64_t row_max = nrows < a_size ? nrows : a_size; - - return (row_max * nn * sizeof(double)) + (128) + (64 * row_max); -} - -/** @brief minimal size of the tmp_space */ -EXPORT uint64_t fft64_vmp_apply_dft_to_dft_tmp_bytes(const MODULE* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -) { - const uint64_t row_max = nrows < a_size ? nrows : a_size; - - return (128) + (64 * row_max); -} - -EXPORT void vmp_apply_dft_to_dft(const MODULE* module, // N - VEC_ZNX_DFT* res, const uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - const VMP_PMAT* pmat, const uint64_t nrows, - const uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -) { - module->func.vmp_apply_dft_to_dft(module, res, res_size, a_dft, a_size, pmat, nrows, ncols, tmp_space); -} - -EXPORT uint64_t vmp_apply_dft_to_dft_tmp_bytes(const MODULE* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -) { - return module->func.vmp_apply_dft_to_dft_tmp_bytes(module, res_size, a_size, nrows, ncols); -} - -/** @brief applies a vmp product (result in DFT space) */ -EXPORT void vmp_apply_dft(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -) { - module->func.vmp_apply_dft(module, res, res_size, a, a_size, a_sl, pmat, nrows, ncols, tmp_space); -} - -/** @brief minimal size of the tmp_space */ -EXPORT uint64_t vmp_apply_dft_tmp_bytes(const MODULE* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -) { - return module->func.vmp_apply_dft_tmp_bytes(module, res_size, a_size, nrows, ncols); -}
D spqlios/lib/spqlios/arithmetic/vector_matrix_product_avx.c

@@ -1,137 +0,0 @@

-#include <string.h> - -#include "../reim4/reim4_arithmetic.h" -#include "vec_znx_arithmetic_private.h" - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void fft64_vmp_prepare_contiguous_avx(const MODULE* module, // N - VMP_PMAT* pmat, // output - const int64_t* mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -) { - // there is an edge case if nn < 8 - const uint64_t nn = module->nn; - const uint64_t m = module->m; - - double* output_mat = (double*)pmat; - double* start_addr = (double*)pmat; - uint64_t offset = nrows * ncols * 8; - - if (nn >= 8) { - for (uint64_t row_i = 0; row_i < nrows; row_i++) { - for (uint64_t col_i = 0; col_i < ncols; col_i++) { - reim_from_znx64(module->mod.fft64.p_conv, (SVP_PPOL*)tmp_space, mat + (row_i * ncols + col_i) * nn); - reim_fft(module->mod.fft64.p_fft, (double*)tmp_space); - - if (col_i == (ncols - 1) && (ncols % 2 == 1)) { - // special case: last column out of an odd column number - start_addr = output_mat + col_i * nrows * 8 // col == ncols-1 - + row_i * 8; - } else { - // general case: columns go by pair - start_addr = output_mat + (col_i / 2) * (2 * nrows) * 8 // second: col pair index - + row_i * 2 * 8 // third: row index - + (col_i % 2) * 8; - } - - for (uint64_t blk_i = 0; blk_i < m / 4; blk_i++) { - // extract blk from tmp and save it - reim4_extract_1blk_from_reim_avx(m, blk_i, start_addr + blk_i * offset, (double*)tmp_space); - } - } - } - } else { - for (uint64_t row_i = 0; row_i < nrows; row_i++) { - for (uint64_t col_i = 0; col_i < ncols; col_i++) { - double* res = (double*)pmat + (col_i * nrows + row_i) * nn; - reim_from_znx64(module->mod.fft64.p_conv, (SVP_PPOL*)res, mat + (row_i * ncols + col_i) * nn); - reim_fft(module->mod.fft64.p_fft, res); - } - } - } -} - -/** @brief applies a vmp product (result in DFT space) */ -EXPORT void fft64_vmp_apply_dft_avx(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -) { - const uint64_t nn = module->nn; - const uint64_t rows = nrows < a_size ? nrows : a_size; - - VEC_ZNX_DFT* a_dft = (VEC_ZNX_DFT*)tmp_space; - uint8_t* new_tmp_space = (uint8_t*)tmp_space + rows * nn * sizeof(double); - - fft64_vec_znx_dft(module, a_dft, rows, a, a_size, a_sl); - fft64_vmp_apply_dft_to_dft_avx(module, res, res_size, a_dft, a_size, pmat, nrows, ncols, new_tmp_space); -} - -/** @brief this inner function could be very handy */ -EXPORT void fft64_vmp_apply_dft_to_dft_avx(const MODULE* module, // N - VEC_ZNX_DFT* res, const uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - const VMP_PMAT* pmat, const uint64_t nrows, - const uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -) { - const uint64_t m = module->m; - const uint64_t nn = module->nn; - - double* mat2cols_output = (double*)tmp_space; // 128 bytes - double* extracted_blk = (double*)tmp_space + 16; // 64*min(nrows,a_size) bytes - - double* mat_input = (double*)pmat; - double* vec_input = (double*)a_dft; - double* vec_output = (double*)res; - - const uint64_t row_max = nrows < a_size ? nrows : a_size; - const uint64_t col_max = ncols < res_size ? ncols : res_size; - - if (nn >= 8) { - for (uint64_t blk_i = 0; blk_i < m / 4; blk_i++) { - double* mat_blk_start = mat_input + blk_i * (8 * nrows * ncols); - - reim4_extract_1blk_from_contiguous_reim_avx(m, row_max, blk_i, (double*)extracted_blk, (double*)a_dft); - // apply mat2cols - for (uint64_t col_i = 0; col_i < col_max - 1; col_i += 2) { - uint64_t col_offset = col_i * (8 * nrows); - reim4_vec_mat2cols_product_avx2(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - - reim4_save_1blk_to_reim_avx(m, blk_i, vec_output + col_i * nn, mat2cols_output); - reim4_save_1blk_to_reim_avx(m, blk_i, vec_output + (col_i + 1) * nn, mat2cols_output + 8); - } - - // check if col_max is odd, then special case - if (col_max % 2 == 1) { - uint64_t last_col = col_max - 1; - uint64_t col_offset = last_col * (8 * nrows); - - // the last column is alone in the pmat: vec_mat1col - if (ncols == col_max) - reim4_vec_mat1col_product_avx2(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - else { - // the last column is part of a colpair in the pmat: vec_mat2cols and ignore the second position - reim4_vec_mat2cols_product_avx2(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - } - reim4_save_1blk_to_reim_avx(m, blk_i, vec_output + last_col * nn, mat2cols_output); - } - } - } else { - for (uint64_t col_i = 0; col_i < col_max; col_i++) { - double* pmat_col = mat_input + col_i * nrows * nn; - for (uint64_t row_i = 0; row_i < 1; row_i++) { - reim_fftvec_mul(module->mod.fft64.mul_fft, vec_output + col_i * nn, vec_input + row_i * nn, - pmat_col + row_i * nn); - } - for (uint64_t row_i = 1; row_i < row_max; row_i++) { - reim_fftvec_addmul(module->mod.fft64.p_addmul, vec_output + col_i * nn, vec_input + row_i * nn, - pmat_col + row_i * nn); - } - } - } - - // zero out remaining bytes - memset(vec_output + col_max * nn, 0, (res_size - col_max) * nn * sizeof(double)); -}
D spqlios/lib/spqlios/arithmetic/zn_api.c

@@ -1,169 +0,0 @@

-#include <string.h> - -#include "zn_arithmetic_private.h" - -void default_init_z_module_precomp(MOD_Z* module) { - // Add here initialization of items that are in the precomp -} - -void default_finalize_z_module_precomp(MOD_Z* module) { - // Add here deleters for items that are in the precomp -} - -void default_init_z_module_vtable(MOD_Z* module) { - // Add function pointers here - module->vtable.i8_approxdecomp_from_tndbl = default_i8_approxdecomp_from_tndbl_ref; - module->vtable.i16_approxdecomp_from_tndbl = default_i16_approxdecomp_from_tndbl_ref; - module->vtable.i32_approxdecomp_from_tndbl = default_i32_approxdecomp_from_tndbl_ref; - module->vtable.zn32_vmp_prepare_contiguous = default_zn32_vmp_prepare_contiguous_ref; - module->vtable.zn32_vmp_apply_i8 = default_zn32_vmp_apply_i8_ref; - module->vtable.zn32_vmp_apply_i16 = default_zn32_vmp_apply_i16_ref; - module->vtable.zn32_vmp_apply_i32 = default_zn32_vmp_apply_i32_ref; - module->vtable.dbl_to_tn32 = dbl_to_tn32_ref; - module->vtable.tn32_to_dbl = tn32_to_dbl_ref; - module->vtable.dbl_round_to_i32 = dbl_round_to_i32_ref; - module->vtable.i32_to_dbl = i32_to_dbl_ref; - module->vtable.dbl_round_to_i64 = dbl_round_to_i64_ref; - module->vtable.i64_to_dbl = i64_to_dbl_ref; - - // Add optimized function pointers here - if (CPU_SUPPORTS("avx")) { - module->vtable.zn32_vmp_apply_i8 = default_zn32_vmp_apply_i8_avx; - module->vtable.zn32_vmp_apply_i16 = default_zn32_vmp_apply_i16_avx; - module->vtable.zn32_vmp_apply_i32 = default_zn32_vmp_apply_i32_avx; - } -} - -void init_z_module_info(MOD_Z* module, // - Z_MODULE_TYPE mtype) { - memset(module, 0, sizeof(MOD_Z)); - module->mtype = mtype; - switch (mtype) { - case DEFAULT: - default_init_z_module_precomp(module); - default_init_z_module_vtable(module); - break; - default: - NOT_SUPPORTED(); // unknown mtype - } -} - -void finalize_z_module_info(MOD_Z* module) { - if (module->custom) module->custom_deleter(module->custom); - switch (module->mtype) { - case DEFAULT: - default_finalize_z_module_precomp(module); - // fft64_finalize_rnx_module_vtable(module); // nothing to finalize - break; - default: - NOT_SUPPORTED(); // unknown mtype - } -} - -EXPORT MOD_Z* new_z_module_info(Z_MODULE_TYPE mtype) { - MOD_Z* res = (MOD_Z*)malloc(sizeof(MOD_Z)); - init_z_module_info(res, mtype); - return res; -} - -EXPORT void delete_z_module_info(MOD_Z* module_info) { - finalize_z_module_info(module_info); - free(module_info); -} - -//////////////// wrappers ////////////////// - -/** @brief sets res = gadget_decompose(a) (int8_t* output) */ -EXPORT void i8_approxdecomp_from_tndbl(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int8_t* res, uint64_t res_size, // res (in general, size ell.a_size) - const double* a, uint64_t a_size) { // a - module->vtable.i8_approxdecomp_from_tndbl(module, gadget, res, res_size, a, a_size); -} - -/** @brief sets res = gadget_decompose(a) (int16_t* output) */ -EXPORT void i16_approxdecomp_from_tndbl(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int16_t* res, uint64_t res_size, // res (in general, size ell.a_size) - const double* a, uint64_t a_size) { // a - module->vtable.i16_approxdecomp_from_tndbl(module, gadget, res, res_size, a, a_size); -} -/** @brief sets res = gadget_decompose(a) (int32_t* output) */ -EXPORT void i32_approxdecomp_from_tndbl(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int32_t* res, uint64_t res_size, // res (in general, size ell.a_size) - const double* a, uint64_t a_size) { // a - module->vtable.i32_approxdecomp_from_tndbl(module, gadget, res, res_size, a, a_size); -} - -EXPORT void zn32_vmp_prepare_contiguous( // - const MOD_Z* module, - ZN32_VMP_PMAT* pmat, // output - const int32_t* mat, uint64_t nrows, uint64_t ncols) { // a - module->vtable.zn32_vmp_prepare_contiguous(module, pmat, mat, nrows, ncols); -} - -/** @brief applies a vmp product (int32_t* input) */ -EXPORT void zn32_vmp_apply_i32(const MOD_Z* module, int32_t* res, uint64_t res_size, const int32_t* a, uint64_t a_size, - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols) { - module->vtable.zn32_vmp_apply_i32(module, res, res_size, a, a_size, pmat, nrows, ncols); -} -/** @brief applies a vmp product (int16_t* input) */ -EXPORT void zn32_vmp_apply_i16(const MOD_Z* module, int32_t* res, uint64_t res_size, const int16_t* a, uint64_t a_size, - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols) { - module->vtable.zn32_vmp_apply_i16(module, res, res_size, a, a_size, pmat, nrows, ncols); -} - -/** @brief applies a vmp product (int8_t* input) */ -EXPORT void zn32_vmp_apply_i8(const MOD_Z* module, int32_t* res, uint64_t res_size, const int8_t* a, uint64_t a_size, - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols) { - module->vtable.zn32_vmp_apply_i8(module, res, res_size, a, a_size, pmat, nrows, ncols); -} - -/** reduction mod 1, output in torus32 space */ -EXPORT void dbl_to_tn32(const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -) { - module->vtable.dbl_to_tn32(module, res, res_size, a, a_size); -} - -/** real centerlift mod 1, output in double space */ -EXPORT void tn32_to_dbl(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int32_t* a, uint64_t a_size // a -) { - module->vtable.tn32_to_dbl(module, res, res_size, a, a_size); -} - -/** round to the nearest int, output in i32 space */ -EXPORT void dbl_round_to_i32(const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -) { - module->vtable.dbl_round_to_i32(module, res, res_size, a, a_size); -} - -/** small int (int32 space) to double */ -EXPORT void i32_to_dbl(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int32_t* a, uint64_t a_size // a -) { - module->vtable.i32_to_dbl(module, res, res_size, a, a_size); -} - -/** round to the nearest int, output in int64 space */ -EXPORT void dbl_round_to_i64(const MOD_Z* module, // - int64_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -) { - module->vtable.dbl_round_to_i64(module, res, res_size, a, a_size); -} - -/** small int (int64 space, <= 2^50) to double */ -EXPORT void i64_to_dbl(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size // a -) { - module->vtable.i64_to_dbl(module, res, res_size, a, a_size); -}
D spqlios/lib/spqlios/arithmetic/zn_approxdecomp_ref.c

@@ -1,81 +0,0 @@

-#include <memory.h> - -#include "zn_arithmetic_private.h" - -EXPORT TNDBL_APPROXDECOMP_GADGET* new_tndbl_approxdecomp_gadget(const MOD_Z* module, // - uint64_t k, uint64_t ell) { - if (k * ell > 50) { - return spqlios_error("approx decomposition requested is too precise for doubles"); - } - if (k < 1) { - return spqlios_error("approx decomposition supports k>=1"); - } - TNDBL_APPROXDECOMP_GADGET* res = malloc(sizeof(TNDBL_APPROXDECOMP_GADGET)); - memset(res, 0, sizeof(TNDBL_APPROXDECOMP_GADGET)); - res->k = k; - res->ell = ell; - double add_cst = INT64_C(3) << (51 - k * ell); - for (uint64_t i = 0; i < ell; ++i) { - add_cst += pow(2., -(double)(i * k + 1)); - } - res->add_cst = add_cst; - res->and_mask = (UINT64_C(1) << k) - 1; - res->sub_cst = UINT64_C(1) << (k - 1); - for (uint64_t i = 0; i < ell; ++i) res->rshifts[i] = (ell - 1 - i) * k; - return res; -} -EXPORT void delete_tndbl_approxdecomp_gadget(TNDBL_APPROXDECOMP_GADGET* ptr) { free(ptr); } - -EXPORT int default_init_tndbl_approxdecomp_gadget(const MOD_Z* module, // - TNDBL_APPROXDECOMP_GADGET* res, // - uint64_t k, uint64_t ell) { - return 0; -} - -typedef union { - double dv; - uint64_t uv; -} du_t; - -#define IMPL_ixx_approxdecomp_from_tndbl_ref(ITYPE) \ - if (res_size != a_size * gadget->ell) NOT_IMPLEMENTED(); \ - const uint64_t ell = gadget->ell; \ - const double add_cst = gadget->add_cst; \ - const uint8_t* const rshifts = gadget->rshifts; \ - const ITYPE and_mask = gadget->and_mask; \ - const ITYPE sub_cst = gadget->sub_cst; \ - ITYPE* rr = res; \ - const double* aa = a; \ - const double* aaend = a + a_size; \ - while (aa < aaend) { \ - du_t t = {.dv = *aa + add_cst}; \ - for (uint64_t i = 0; i < ell; ++i) { \ - ITYPE v = (ITYPE)(t.uv >> rshifts[i]); \ - *rr = (v & and_mask) - sub_cst; \ - ++rr; \ - } \ - ++aa; \ - } - -/** @brief sets res = gadget_decompose(a) (int8_t* output) */ -EXPORT void default_i8_approxdecomp_from_tndbl_ref(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int8_t* res, uint64_t res_size, // res (in general, size ell.a_size) - const double* a, uint64_t a_size // -){IMPL_ixx_approxdecomp_from_tndbl_ref(int8_t)} - -/** @brief sets res = gadget_decompose(a) (int16_t* output) */ -EXPORT void default_i16_approxdecomp_from_tndbl_ref(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int16_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -){IMPL_ixx_approxdecomp_from_tndbl_ref(int16_t)} - -/** @brief sets res = gadget_decompose(a) (int32_t* output) */ -EXPORT void default_i32_approxdecomp_from_tndbl_ref(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int32_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -) { - IMPL_ixx_approxdecomp_from_tndbl_ref(int32_t) -}
D spqlios/lib/spqlios/arithmetic/zn_arithmetic.h

@@ -1,135 +0,0 @@

-#ifndef SPQLIOS_ZN_ARITHMETIC_H -#define SPQLIOS_ZN_ARITHMETIC_H - -#include <stdint.h> - -#include "../commons.h" - -typedef enum z_module_type_t { DEFAULT } Z_MODULE_TYPE; - -/** @brief opaque structure that describes the module and the hardware */ -typedef struct z_module_info_t MOD_Z; - -/** - * @brief obtain a module info for ring dimension N - * the module-info knows about: - * - the dimension N (or the complex dimension m=N/2) - * - any moduleuted fft or ntt items - * - the hardware (avx, arm64, x86, ...) - */ -EXPORT MOD_Z* new_z_module_info(Z_MODULE_TYPE mode); -EXPORT void delete_z_module_info(MOD_Z* module_info); - -typedef struct tndbl_approxdecomp_gadget_t TNDBL_APPROXDECOMP_GADGET; - -EXPORT TNDBL_APPROXDECOMP_GADGET* new_tndbl_approxdecomp_gadget(const MOD_Z* module, // - uint64_t k, - uint64_t ell); // base 2^k, and size - -EXPORT void delete_tndbl_approxdecomp_gadget(TNDBL_APPROXDECOMP_GADGET* ptr); - -/** @brief sets res = gadget_decompose(a) (int8_t* output) */ -EXPORT void i8_approxdecomp_from_tndbl(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int8_t* res, uint64_t res_size, // res (in general, size ell.a_size) - const double* a, uint64_t a_size); // a - -/** @brief sets res = gadget_decompose(a) (int16_t* output) */ -EXPORT void i16_approxdecomp_from_tndbl(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int16_t* res, uint64_t res_size, // res (in general, size ell.a_size) - const double* a, uint64_t a_size); // a -/** @brief sets res = gadget_decompose(a) (int32_t* output) */ -EXPORT void i32_approxdecomp_from_tndbl(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int32_t* res, uint64_t res_size, // res (in general, size ell.a_size) - const double* a, uint64_t a_size); // a - -/** @brief opaque type that represents a prepared matrix */ -typedef struct zn32_vmp_pmat_t ZN32_VMP_PMAT; - -/** @brief size in bytes of a prepared matrix (for custom allocation) */ -EXPORT uint64_t bytes_of_zn32_vmp_pmat(const MOD_Z* module, // N - uint64_t nrows, uint64_t ncols); // dimensions - -/** @brief allocates a prepared matrix (release with delete_zn32_vmp_pmat) */ -EXPORT ZN32_VMP_PMAT* new_zn32_vmp_pmat(const MOD_Z* module, // N - uint64_t nrows, uint64_t ncols); // dimensions - -/** @brief deletes a prepared matrix (release with free) */ -EXPORT void delete_zn32_vmp_pmat(ZN32_VMP_PMAT* ptr); // dimensions - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void zn32_vmp_prepare_contiguous( // - const MOD_Z* module, - ZN32_VMP_PMAT* pmat, // output - const int32_t* mat, uint64_t nrows, uint64_t ncols); // a - -/** @brief applies a vmp product (int32_t* input) */ -EXPORT void zn32_vmp_apply_i32( // - const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const int32_t* a, uint64_t a_size, // a - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols); // prep matrix - -/** @brief applies a vmp product (int16_t* input) */ -EXPORT void zn32_vmp_apply_i16( // - const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const int16_t* a, uint64_t a_size, // a - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols); // prep matrix - -/** @brief applies a vmp product (int8_t* input) */ -EXPORT void zn32_vmp_apply_i8( // - const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const int8_t* a, uint64_t a_size, // a - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols); // prep matrix - -// explicit conversions - -/** reduction mod 1, output in torus32 space */ -EXPORT void dbl_to_tn32(const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -); - -/** real centerlift mod 1, output in double space */ -EXPORT void tn32_to_dbl(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int32_t* a, uint64_t a_size // a -); - -/** round to the nearest int, output in i32 space. - * WARNING: ||a||_inf must be <= 2^18 in this function - */ -EXPORT void dbl_round_to_i32(const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -); - -/** small int (int32 space) to double - * WARNING: ||a||_inf must be <= 2^18 in this function - */ -EXPORT void i32_to_dbl(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int32_t* a, uint64_t a_size // a -); - -/** round to the nearest int, output in int64 space - * WARNING: ||a||_inf must be <= 2^50 in this function - */ -EXPORT void dbl_round_to_i64(const MOD_Z* module, // - int64_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -); - -/** small int (int64 space, <= 2^50) to double - * WARNING: ||a||_inf must be <= 2^50 in this function - */ -EXPORT void i64_to_dbl(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size // a -); - -#endif // SPQLIOS_ZN_ARITHMETIC_H
D spqlios/lib/spqlios/arithmetic/zn_arithmetic_plugin.h

@@ -1,39 +0,0 @@

-#ifndef SPQLIOS_ZN_ARITHMETIC_PLUGIN_H -#define SPQLIOS_ZN_ARITHMETIC_PLUGIN_H - -#include "zn_arithmetic.h" - -typedef typeof(i8_approxdecomp_from_tndbl) I8_APPROXDECOMP_FROM_TNDBL_F; -typedef typeof(i16_approxdecomp_from_tndbl) I16_APPROXDECOMP_FROM_TNDBL_F; -typedef typeof(i32_approxdecomp_from_tndbl) I32_APPROXDECOMP_FROM_TNDBL_F; -typedef typeof(bytes_of_zn32_vmp_pmat) BYTES_OF_ZN32_VMP_PMAT_F; -typedef typeof(zn32_vmp_prepare_contiguous) ZN32_VMP_PREPARE_CONTIGUOUS_F; -typedef typeof(zn32_vmp_apply_i32) ZN32_VMP_APPLY_I32_F; -typedef typeof(zn32_vmp_apply_i16) ZN32_VMP_APPLY_I16_F; -typedef typeof(zn32_vmp_apply_i8) ZN32_VMP_APPLY_I8_F; -typedef typeof(dbl_to_tn32) DBL_TO_TN32_F; -typedef typeof(tn32_to_dbl) TN32_TO_DBL_F; -typedef typeof(dbl_round_to_i32) DBL_ROUND_TO_I32_F; -typedef typeof(i32_to_dbl) I32_TO_DBL_F; -typedef typeof(dbl_round_to_i64) DBL_ROUND_TO_I64_F; -typedef typeof(i64_to_dbl) I64_TO_DBL_F; - -typedef struct z_module_vtable_t Z_MODULE_VTABLE; -struct z_module_vtable_t { - I8_APPROXDECOMP_FROM_TNDBL_F* i8_approxdecomp_from_tndbl; - I16_APPROXDECOMP_FROM_TNDBL_F* i16_approxdecomp_from_tndbl; - I32_APPROXDECOMP_FROM_TNDBL_F* i32_approxdecomp_from_tndbl; - BYTES_OF_ZN32_VMP_PMAT_F* bytes_of_zn32_vmp_pmat; - ZN32_VMP_PREPARE_CONTIGUOUS_F* zn32_vmp_prepare_contiguous; - ZN32_VMP_APPLY_I32_F* zn32_vmp_apply_i32; - ZN32_VMP_APPLY_I16_F* zn32_vmp_apply_i16; - ZN32_VMP_APPLY_I8_F* zn32_vmp_apply_i8; - DBL_TO_TN32_F* dbl_to_tn32; - TN32_TO_DBL_F* tn32_to_dbl; - DBL_ROUND_TO_I32_F* dbl_round_to_i32; - I32_TO_DBL_F* i32_to_dbl; - DBL_ROUND_TO_I64_F* dbl_round_to_i64; - I64_TO_DBL_F* i64_to_dbl; -}; - -#endif // SPQLIOS_ZN_ARITHMETIC_PLUGIN_H
D spqlios/lib/spqlios/arithmetic/zn_arithmetic_private.h

@@ -1,150 +0,0 @@

-#ifndef SPQLIOS_ZN_ARITHMETIC_PRIVATE_H -#define SPQLIOS_ZN_ARITHMETIC_PRIVATE_H - -#include "../commons_private.h" -#include "zn_arithmetic.h" -#include "zn_arithmetic_plugin.h" - -typedef struct main_z_module_precomp_t MAIN_Z_MODULE_PRECOMP; -struct main_z_module_precomp_t { - // TODO -}; - -typedef union z_module_precomp_t Z_MODULE_PRECOMP; -union z_module_precomp_t { - MAIN_Z_MODULE_PRECOMP main; -}; - -void main_init_z_module_precomp(MOD_Z* module); - -void main_finalize_z_module_precomp(MOD_Z* module); - -/** @brief opaque structure that describes the modules (RnX,ZnX,TnX) and the hardware */ -struct z_module_info_t { - Z_MODULE_TYPE mtype; - Z_MODULE_VTABLE vtable; - Z_MODULE_PRECOMP precomp; - void* custom; - void (*custom_deleter)(void*); -}; - -void init_z_module_info(MOD_Z* module, Z_MODULE_TYPE mtype); - -void main_init_z_module_vtable(MOD_Z* module); - -struct tndbl_approxdecomp_gadget_t { - uint64_t k; - uint64_t ell; - double add_cst; // 3.2^51-(K.ell) + 1/2.(sum 2^-(i+1)K) - int64_t and_mask; // (2^K)-1 - int64_t sub_cst; // 2^(K-1) - uint8_t rshifts[64]; // 2^(ell-1-i).K for i in [0:ell-1] -}; - -/** @brief sets res = gadget_decompose(a) (int8_t* output) */ -EXPORT void default_i8_approxdecomp_from_tndbl_ref(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int8_t* res, uint64_t res_size, // res (in general, size ell.a_size) - const double* a, uint64_t a_size); // a - -/** @brief sets res = gadget_decompose(a) (int16_t* output) */ -EXPORT void default_i16_approxdecomp_from_tndbl_ref(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int16_t* res, - uint64_t res_size, // res (in general, size ell.a_size) - const double* a, uint64_t a_size); // a -/** @brief sets res = gadget_decompose(a) (int32_t* output) */ -EXPORT void default_i32_approxdecomp_from_tndbl_ref(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int32_t* res, - uint64_t res_size, // res (in general, size ell.a_size) - const double* a, uint64_t a_size); // a - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void default_zn32_vmp_prepare_contiguous_ref( // - const MOD_Z* module, - ZN32_VMP_PMAT* pmat, // output - const int32_t* mat, uint64_t nrows, uint64_t ncols // a -); - -/** @brief applies a vmp product (int32_t* input) */ -EXPORT void default_zn32_vmp_apply_i32_ref( // - const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const int32_t* a, uint64_t a_size, // a - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols); // prep matrix - -/** @brief applies a vmp product (int16_t* input) */ -EXPORT void default_zn32_vmp_apply_i16_ref( // - const MOD_Z* module, // N - int32_t* res, uint64_t res_size, // res - const int16_t* a, uint64_t a_size, // a - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols); // prep matrix - -/** @brief applies a vmp product (int8_t* input) */ -EXPORT void default_zn32_vmp_apply_i8_ref( // - const MOD_Z* module, // N - int32_t* res, uint64_t res_size, // res - const int8_t* a, uint64_t a_size, // a - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols); // prep matrix - -/** @brief applies a vmp product (int32_t* input) */ -EXPORT void default_zn32_vmp_apply_i32_avx( // - const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const int32_t* a, uint64_t a_size, // a - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols); // prep matrix - -/** @brief applies a vmp product (int16_t* input) */ -EXPORT void default_zn32_vmp_apply_i16_avx( // - const MOD_Z* module, // N - int32_t* res, uint64_t res_size, // res - const int16_t* a, uint64_t a_size, // a - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols); // prep matrix - -/** @brief applies a vmp product (int8_t* input) */ -EXPORT void default_zn32_vmp_apply_i8_avx( // - const MOD_Z* module, // N - int32_t* res, uint64_t res_size, // res - const int8_t* a, uint64_t a_size, // a - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols); // prep matrix - -// explicit conversions - -/** reduction mod 1, output in torus32 space */ -EXPORT void dbl_to_tn32_ref(const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -); - -/** real centerlift mod 1, output in double space */ -EXPORT void tn32_to_dbl_ref(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int32_t* a, uint64_t a_size // a -); - -/** round to the nearest int, output in i32 space */ -EXPORT void dbl_round_to_i32_ref(const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -); - -/** small int (int32 space) to double */ -EXPORT void i32_to_dbl_ref(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int32_t* a, uint64_t a_size // a -); - -/** round to the nearest int, output in int64 space */ -EXPORT void dbl_round_to_i64_ref(const MOD_Z* module, // - int64_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -); - -/** small int (int64 space) to double */ -EXPORT void i64_to_dbl_ref(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size // a -); - -#endif // SPQLIOS_ZN_ARITHMETIC_PRIVATE_H
D spqlios/lib/spqlios/arithmetic/zn_conversions_ref.c

@@ -1,108 +0,0 @@

-#include <memory.h> - -#include "zn_arithmetic_private.h" - -typedef union { - double dv; - int64_t s64v; - int32_t s32v; - uint64_t u64v; - uint32_t u32v; -} di_t; - -/** reduction mod 1, output in torus32 space */ -EXPORT void dbl_to_tn32_ref(const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -) { - static const double ADD_CST = 0.5 + (double)(INT64_C(3) << (51 - 32)); - static const int32_t XOR_CST = (INT32_C(1) << 31); - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - di_t t = {.dv = a[i] + ADD_CST}; - res[i] = t.s32v ^ XOR_CST; - } - memset(res + msize, 0, (res_size - msize) * sizeof(int32_t)); -} - -/** real centerlift mod 1, output in double space */ -EXPORT void tn32_to_dbl_ref(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int32_t* a, uint64_t a_size // a -) { - static const uint32_t XOR_CST = (UINT32_C(1) << 31); - static const di_t OR_CST = {.dv = (double)(INT64_C(1) << (52 - 32))}; - static const double SUB_CST = 0.5 + (double)(INT64_C(1) << (52 - 32)); - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - uint32_t ai = a[i] ^ XOR_CST; - di_t t = {.u64v = OR_CST.u64v | (uint64_t)ai}; - res[i] = t.dv - SUB_CST; - } - memset(res + msize, 0, (res_size - msize) * sizeof(double)); -} - -/** round to the nearest int, output in i32 space */ -EXPORT void dbl_round_to_i32_ref(const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -) { - static const double ADD_CST = (double)((INT64_C(3) << (51)) + (INT64_C(1) << (31))); - static const int32_t XOR_CST = INT32_C(1) << 31; - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - di_t t = {.dv = a[i] + ADD_CST}; - res[i] = t.s32v ^ XOR_CST; - } - memset(res + msize, 0, (res_size - msize) * sizeof(int32_t)); -} - -/** small int (int32 space) to double */ -EXPORT void i32_to_dbl_ref(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int32_t* a, uint64_t a_size // a -) { - static const uint32_t XOR_CST = (UINT32_C(1) << 31); - static const di_t OR_CST = {.dv = (double)(INT64_C(1) << 52)}; - static const double SUB_CST = (double)((INT64_C(1) << 52) + (INT64_C(1) << 31)); - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - uint32_t ai = a[i] ^ XOR_CST; - di_t t = {.u64v = OR_CST.u64v | (uint64_t)ai}; - res[i] = t.dv - SUB_CST; - } - memset(res + msize, 0, (res_size - msize) * sizeof(double)); -} - -/** round to the nearest int, output in int64 space */ -EXPORT void dbl_round_to_i64_ref(const MOD_Z* module, // - int64_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -) { - static const double ADD_CST = (double)(INT64_C(3) << (51)); - static const int64_t AND_CST = (INT64_C(1) << 52) - 1; - static const int64_t SUB_CST = INT64_C(1) << 51; - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - di_t t = {.dv = a[i] + ADD_CST}; - res[i] = (t.s64v & AND_CST) - SUB_CST; - } - memset(res + msize, 0, (res_size - msize) * sizeof(int64_t)); -} - -/** small int (int64 space) to double */ -EXPORT void i64_to_dbl_ref(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size // a -) { - static const uint64_t ADD_CST = UINT64_C(1) << 51; - static const uint64_t AND_CST = (UINT64_C(1) << 52) - 1; - static const di_t OR_CST = {.dv = (INT64_C(1) << 52)}; - static const double SUB_CST = INT64_C(3) << 51; - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - di_t t = {.u64v = ((a[i] + ADD_CST) & AND_CST) | OR_CST.u64v}; - res[i] = t.dv - SUB_CST; - } - memset(res + msize, 0, (res_size - msize) * sizeof(double)); -}
D spqlios/lib/spqlios/arithmetic/zn_vmp_int16_avx.c

@@ -1,4 +0,0 @@

-#define INTTYPE int16_t -#define INTSN i16 - -#include "zn_vmp_int32_avx.c"
D spqlios/lib/spqlios/arithmetic/zn_vmp_int16_ref.c

@@ -1,4 +0,0 @@

-#define INTTYPE int16_t -#define INTSN i16 - -#include "zn_vmp_int32_ref.c"
D spqlios/lib/spqlios/arithmetic/zn_vmp_int32_avx.c

@@ -1,223 +0,0 @@

-// This file is actually a template: it will be compiled multiple times with -// different INTTYPES -#ifndef INTTYPE -#define INTTYPE int32_t -#define INTSN i32 -#endif - -#include <immintrin.h> -#include <memory.h> - -#include "zn_arithmetic_private.h" - -#define concat_inner(aa, bb, cc) aa##_##bb##_##cc -#define concat(aa, bb, cc) concat_inner(aa, bb, cc) -#define zn32_vec_fn(cc) concat(zn32_vec, INTSN, cc) - -static void zn32_vec_mat32cols_avx_prefetch(uint64_t nrows, int32_t* res, const INTTYPE* a, const int32_t* b) { - if (nrows == 0) { - memset(res, 0, 32 * sizeof(int32_t)); - return; - } - const int32_t* bb = b; - const int32_t* pref_bb = b; - const uint64_t pref_iters = 128; - const uint64_t pref_start = pref_iters < nrows ? pref_iters : nrows; - const uint64_t pref_last = pref_iters > nrows ? 0 : nrows - pref_iters; - // let's do some prefetching of the GSW key, since on some cpus, - // it helps - for (uint64_t i = 0; i < pref_start; ++i) { - __builtin_prefetch(pref_bb, 0, _MM_HINT_T0); - __builtin_prefetch(pref_bb + 16, 0, _MM_HINT_T0); - pref_bb += 32; - } - // we do the first iteration - __m256i x = _mm256_set1_epi32(a[0]); - __m256i r0 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb))); - __m256i r1 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 8))); - __m256i r2 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 16))); - __m256i r3 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 24))); - bb += 32; - uint64_t row = 1; - for (; // - row < pref_last; // - ++row, bb += 32) { - // prefetch the next iteration - __builtin_prefetch(pref_bb, 0, _MM_HINT_T0); - __builtin_prefetch(pref_bb + 16, 0, _MM_HINT_T0); - pref_bb += 32; - INTTYPE ai = a[row]; - if (ai == 0) continue; - x = _mm256_set1_epi32(ai); - r0 = _mm256_add_epi32(r0, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb)))); - r1 = _mm256_add_epi32(r1, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 8)))); - r2 = _mm256_add_epi32(r2, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 16)))); - r3 = _mm256_add_epi32(r3, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 24)))); - } - for (; // - row < nrows; // - ++row, bb += 32) { - INTTYPE ai = a[row]; - if (ai == 0) continue; - x = _mm256_set1_epi32(ai); - r0 = _mm256_add_epi32(r0, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb)))); - r1 = _mm256_add_epi32(r1, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 8)))); - r2 = _mm256_add_epi32(r2, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 16)))); - r3 = _mm256_add_epi32(r3, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 24)))); - } - _mm256_storeu_si256((__m256i*)(res), r0); - _mm256_storeu_si256((__m256i*)(res + 8), r1); - _mm256_storeu_si256((__m256i*)(res + 16), r2); - _mm256_storeu_si256((__m256i*)(res + 24), r3); -} - -void zn32_vec_fn(mat32cols_avx)(uint64_t nrows, int32_t* res, const INTTYPE* a, const int32_t* b, uint64_t b_sl) { - if (nrows == 0) { - memset(res, 0, 32 * sizeof(int32_t)); - return; - } - const INTTYPE* aa = a; - const INTTYPE* const aaend = a + nrows; - const int32_t* bb = b; - __m256i x = _mm256_set1_epi32(*aa); - __m256i r0 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb))); - __m256i r1 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 8))); - __m256i r2 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 16))); - __m256i r3 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 24))); - bb += b_sl; - ++aa; - for (; // - aa < aaend; // - bb += b_sl, ++aa) { - INTTYPE ai = *aa; - if (ai == 0) continue; - x = _mm256_set1_epi32(ai); - r0 = _mm256_add_epi32(r0, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb)))); - r1 = _mm256_add_epi32(r1, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 8)))); - r2 = _mm256_add_epi32(r2, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 16)))); - r3 = _mm256_add_epi32(r3, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 24)))); - } - _mm256_storeu_si256((__m256i*)(res), r0); - _mm256_storeu_si256((__m256i*)(res + 8), r1); - _mm256_storeu_si256((__m256i*)(res + 16), r2); - _mm256_storeu_si256((__m256i*)(res + 24), r3); -} - -void zn32_vec_fn(mat24cols_avx)(uint64_t nrows, int32_t* res, const INTTYPE* a, const int32_t* b, uint64_t b_sl) { - if (nrows == 0) { - memset(res, 0, 24 * sizeof(int32_t)); - return; - } - const INTTYPE* aa = a; - const INTTYPE* const aaend = a + nrows; - const int32_t* bb = b; - __m256i x = _mm256_set1_epi32(*aa); - __m256i r0 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb))); - __m256i r1 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 8))); - __m256i r2 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 16))); - bb += b_sl; - ++aa; - for (; // - aa < aaend; // - bb += b_sl, ++aa) { - INTTYPE ai = *aa; - if (ai == 0) continue; - x = _mm256_set1_epi32(ai); - r0 = _mm256_add_epi32(r0, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb)))); - r1 = _mm256_add_epi32(r1, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 8)))); - r2 = _mm256_add_epi32(r2, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 16)))); - } - _mm256_storeu_si256((__m256i*)(res), r0); - _mm256_storeu_si256((__m256i*)(res + 8), r1); - _mm256_storeu_si256((__m256i*)(res + 16), r2); -} -void zn32_vec_fn(mat16cols_avx)(uint64_t nrows, int32_t* res, const INTTYPE* a, const int32_t* b, uint64_t b_sl) { - if (nrows == 0) { - memset(res, 0, 16 * sizeof(int32_t)); - return; - } - const INTTYPE* aa = a; - const INTTYPE* const aaend = a + nrows; - const int32_t* bb = b; - __m256i x = _mm256_set1_epi32(*aa); - __m256i r0 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb))); - __m256i r1 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 8))); - bb += b_sl; - ++aa; - for (; // - aa < aaend; // - bb += b_sl, ++aa) { - INTTYPE ai = *aa; - if (ai == 0) continue; - x = _mm256_set1_epi32(ai); - r0 = _mm256_add_epi32(r0, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb)))); - r1 = _mm256_add_epi32(r1, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 8)))); - } - _mm256_storeu_si256((__m256i*)(res), r0); - _mm256_storeu_si256((__m256i*)(res + 8), r1); -} - -void zn32_vec_fn(mat8cols_avx)(uint64_t nrows, int32_t* res, const INTTYPE* a, const int32_t* b, uint64_t b_sl) { - if (nrows == 0) { - memset(res, 0, 8 * sizeof(int32_t)); - return; - } - const INTTYPE* aa = a; - const INTTYPE* const aaend = a + nrows; - const int32_t* bb = b; - __m256i x = _mm256_set1_epi32(*aa); - __m256i r0 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb))); - bb += b_sl; - ++aa; - for (; // - aa < aaend; // - bb += b_sl, ++aa) { - INTTYPE ai = *aa; - if (ai == 0) continue; - x = _mm256_set1_epi32(ai); - r0 = _mm256_add_epi32(r0, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb)))); - } - _mm256_storeu_si256((__m256i*)(res), r0); -} - -typedef void (*vm_f)(uint64_t nrows, // - int32_t* res, // - const INTTYPE* a, // - const int32_t* b, uint64_t b_sl // -); -static const vm_f zn32_vec_mat8kcols_avx[4] = { // - zn32_vec_fn(mat8cols_avx), // - zn32_vec_fn(mat16cols_avx), // - zn32_vec_fn(mat24cols_avx), // - zn32_vec_fn(mat32cols_avx)}; - -/** @brief applies a vmp product (int32_t* input) */ -EXPORT void concat(default_zn32_vmp_apply, INTSN, avx)( // - const MOD_Z* module, // - int32_t* res, uint64_t res_size, // - const INTTYPE* a, uint64_t a_size, // - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols) { - const uint64_t rows = a_size < nrows ? a_size : nrows; - const uint64_t cols = res_size < ncols ? res_size : ncols; - const uint64_t ncolblk = cols >> 5; - const uint64_t ncolrem = cols & 31; - // copy the first full blocks - const uint64_t full_blk_size = nrows * 32; - const int32_t* mat = (int32_t*)pmat; - int32_t* rr = res; - for (uint64_t blk = 0; // - blk < ncolblk; // - ++blk, mat += full_blk_size, rr += 32) { - zn32_vec_mat32cols_avx_prefetch(rows, rr, a, mat); - } - // last block - if (ncolrem) { - uint64_t orig_rem = ncols - (ncolblk << 5); - uint64_t b_sl = orig_rem >= 32 ? 32 : orig_rem; - int32_t tmp[32]; - zn32_vec_mat8kcols_avx[(ncolrem - 1) >> 3](rows, tmp, a, mat, b_sl); - memcpy(rr, tmp, ncolrem * sizeof(int32_t)); - } - // trailing bytes - memset(res + cols, 0, (res_size - cols) * sizeof(int32_t)); -}
D spqlios/lib/spqlios/arithmetic/zn_vmp_int32_ref.c

@@ -1,88 +0,0 @@

-// This file is actually a template: it will be compiled multiple times with -// different INTTYPES -#ifndef INTTYPE -#define INTTYPE int32_t -#define INTSN i32 -#endif - -#include <memory.h> - -#include "zn_arithmetic_private.h" - -#define concat_inner(aa, bb, cc) aa##_##bb##_##cc -#define concat(aa, bb, cc) concat_inner(aa, bb, cc) -#define zn32_vec_fn(cc) concat(zn32_vec, INTSN, cc) - -// the ref version shares the same implementation for each fixed column size -// optimized implementations may do something different. -static __always_inline void IMPL_zn32_vec_matcols_ref( - const uint64_t NCOLS, // fixed number of columns - uint64_t nrows, // nrows of b - int32_t* res, // result: size NCOLS, only the first min(b_sl, NCOLS) are relevant - const INTTYPE* a, // a: nrows-sized vector - const int32_t* b, uint64_t b_sl // b: nrows * min(b_sl, NCOLS) matrix -) { - memset(res, 0, NCOLS * sizeof(int32_t)); - for (uint64_t row = 0; row < nrows; ++row) { - int32_t ai = a[row]; - const int32_t* bb = b + row * b_sl; - for (uint64_t i = 0; i < NCOLS; ++i) { - res[i] += ai * bb[i]; - } - } -} - -void zn32_vec_fn(mat32cols_ref)(uint64_t nrows, int32_t* res, const INTTYPE* a, const int32_t* b, uint64_t b_sl) { - IMPL_zn32_vec_matcols_ref(32, nrows, res, a, b, b_sl); -} -void zn32_vec_fn(mat24cols_ref)(uint64_t nrows, int32_t* res, const INTTYPE* a, const int32_t* b, uint64_t b_sl) { - IMPL_zn32_vec_matcols_ref(24, nrows, res, a, b, b_sl); -} -void zn32_vec_fn(mat16cols_ref)(uint64_t nrows, int32_t* res, const INTTYPE* a, const int32_t* b, uint64_t b_sl) { - IMPL_zn32_vec_matcols_ref(16, nrows, res, a, b, b_sl); -} -void zn32_vec_fn(mat8cols_ref)(uint64_t nrows, int32_t* res, const INTTYPE* a, const int32_t* b, uint64_t b_sl) { - IMPL_zn32_vec_matcols_ref(8, nrows, res, a, b, b_sl); -} - -typedef void (*vm_f)(uint64_t nrows, // - int32_t* res, // - const INTTYPE* a, // - const int32_t* b, uint64_t b_sl // -); -static const vm_f zn32_vec_mat8kcols_ref[4] = { // - zn32_vec_fn(mat8cols_ref), // - zn32_vec_fn(mat16cols_ref), // - zn32_vec_fn(mat24cols_ref), // - zn32_vec_fn(mat32cols_ref)}; - -/** @brief applies a vmp product (int32_t* input) */ -EXPORT void concat(default_zn32_vmp_apply, INTSN, ref)( // - const MOD_Z* module, // - int32_t* res, uint64_t res_size, // - const INTTYPE* a, uint64_t a_size, // - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols) { - const uint64_t rows = a_size < nrows ? a_size : nrows; - const uint64_t cols = res_size < ncols ? res_size : ncols; - const uint64_t ncolblk = cols >> 5; - const uint64_t ncolrem = cols & 31; - // copy the first full blocks - const uint32_t full_blk_size = nrows * 32; - const int32_t* mat = (int32_t*)pmat; - int32_t* rr = res; - for (uint64_t blk = 0; // - blk < ncolblk; // - ++blk, mat += full_blk_size, rr += 32) { - zn32_vec_fn(mat32cols_ref)(rows, rr, a, mat, 32); - } - // last block - if (ncolrem) { - uint64_t orig_rem = ncols - (ncolblk << 5); - uint64_t b_sl = orig_rem >= 32 ? 32 : orig_rem; - int32_t tmp[32]; - zn32_vec_mat8kcols_ref[(ncolrem - 1) >> 3](rows, tmp, a, mat, b_sl); - memcpy(rr, tmp, ncolrem * sizeof(int32_t)); - } - // trailing bytes - memset(res + cols, 0, (res_size - cols) * sizeof(int32_t)); -}
D spqlios/lib/spqlios/arithmetic/zn_vmp_int8_avx.c

@@ -1,4 +0,0 @@

-#define INTTYPE int8_t -#define INTSN i8 - -#include "zn_vmp_int32_avx.c"
D spqlios/lib/spqlios/arithmetic/zn_vmp_int8_ref.c

@@ -1,4 +0,0 @@

-#define INTTYPE int8_t -#define INTSN i8 - -#include "zn_vmp_int32_ref.c"
D spqlios/lib/spqlios/arithmetic/zn_vmp_ref.c

@@ -1,138 +0,0 @@

-#include <memory.h> - -#include "zn_arithmetic_private.h" - -/** @brief size in bytes of a prepared matrix (for custom allocation) */ -EXPORT uint64_t bytes_of_zn32_vmp_pmat(const MOD_Z* module, // N - uint64_t nrows, uint64_t ncols // dimensions -) { - return (nrows * ncols + 7) * sizeof(int32_t); -} - -/** @brief allocates a prepared matrix (release with delete_zn32_vmp_pmat) */ -EXPORT ZN32_VMP_PMAT* new_zn32_vmp_pmat(const MOD_Z* module, // N - uint64_t nrows, uint64_t ncols) { - return (ZN32_VMP_PMAT*)spqlios_alloc(bytes_of_zn32_vmp_pmat(module, nrows, ncols)); -} - -/** @brief deletes a prepared matrix (release with free) */ -EXPORT void delete_zn32_vmp_pmat(ZN32_VMP_PMAT* ptr) { spqlios_free(ptr); } - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void default_zn32_vmp_prepare_contiguous_ref( // - const MOD_Z* module, - ZN32_VMP_PMAT* pmat, // output - const int32_t* mat, uint64_t nrows, uint64_t ncols // a -) { - int32_t* const out = (int32_t*)pmat; - const uint64_t nblk = ncols >> 5; - const uint64_t ncols_rem = ncols & 31; - const uint64_t final_elems = (8 - nrows * ncols) & 7; - for (uint64_t blk = 0; blk < nblk; ++blk) { - int32_t* outblk = out + blk * nrows * 32; - const int32_t* srcblk = mat + blk * 32; - for (uint64_t row = 0; row < nrows; ++row) { - int32_t* dest = outblk + row * 32; - const int32_t* src = srcblk + row * ncols; - for (uint64_t i = 0; i < 32; ++i) { - dest[i] = src[i]; - } - } - } - // copy the last block if any - if (ncols_rem) { - int32_t* outblk = out + nblk * nrows * 32; - const int32_t* srcblk = mat + nblk * 32; - for (uint64_t row = 0; row < nrows; ++row) { - int32_t* dest = outblk + row * ncols_rem; - const int32_t* src = srcblk + row * ncols; - for (uint64_t i = 0; i < ncols_rem; ++i) { - dest[i] = src[i]; - } - } - } - // zero-out the final elements that may be accessed - if (final_elems) { - int32_t* f = out + nrows * ncols; - for (uint64_t i = 0; i < final_elems; ++i) { - f[i] = 0; - } - } -} - -#if 0 - -#define IMPL_zn32_vec_ixxx_matyyycols_ref(NCOLS) \ - memset(res, 0, NCOLS * sizeof(int32_t)); \ - for (uint64_t row = 0; row < nrows; ++row) { \ - int32_t ai = a[row]; \ - const int32_t* bb = b + row * b_sl; \ - for (uint64_t i = 0; i < NCOLS; ++i) { \ - res[i] += ai * bb[i]; \ - } \ - } - -#define IMPL_zn32_vec_ixxx_mat8cols_ref() IMPL_zn32_vec_ixxx_matyyycols_ref(8) -#define IMPL_zn32_vec_ixxx_mat16cols_ref() IMPL_zn32_vec_ixxx_matyyycols_ref(16) -#define IMPL_zn32_vec_ixxx_mat24cols_ref() IMPL_zn32_vec_ixxx_matyyycols_ref(24) -#define IMPL_zn32_vec_ixxx_mat32cols_ref() IMPL_zn32_vec_ixxx_matyyycols_ref(32) - -void zn32_vec_i8_mat32cols_ref(uint64_t nrows, int32_t* res, const int8_t* a, const int32_t* b, uint64_t b_sl) { - IMPL_zn32_vec_ixxx_mat32cols_ref() -} -void zn32_vec_i16_mat32cols_ref(uint64_t nrows, int32_t* res, const int16_t* a, const int32_t* b, uint64_t b_sl) { - IMPL_zn32_vec_ixxx_mat32cols_ref() -} - -void zn32_vec_i32_mat32cols_ref(uint64_t nrows, int32_t* res, const int32_t* a, const int32_t* b, uint64_t b_sl) { - IMPL_zn32_vec_ixxx_mat32cols_ref() -} -void zn32_vec_i32_mat24cols_ref(uint64_t nrows, int32_t* res, const int32_t* a, const int32_t* b, uint64_t b_sl) { - IMPL_zn32_vec_ixxx_mat24cols_ref() -} -void zn32_vec_i32_mat16cols_ref(uint64_t nrows, int32_t* res, const int32_t* a, const int32_t* b, uint64_t b_sl) { - IMPL_zn32_vec_ixxx_mat16cols_ref() -} -void zn32_vec_i32_mat8cols_ref(uint64_t nrows, int32_t* res, const int32_t* a, const int32_t* b, uint64_t b_sl) { - IMPL_zn32_vec_ixxx_mat8cols_ref() -} -typedef void (*zn32_vec_i32_mat8kcols_ref_f)(uint64_t nrows, // - int32_t* res, // - const int32_t* a, // - const int32_t* b, uint64_t b_sl // -); -zn32_vec_i32_mat8kcols_ref_f zn32_vec_i32_mat8kcols_ref[4] = { // - zn32_vec_i32_mat8cols_ref, zn32_vec_i32_mat16cols_ref, // - zn32_vec_i32_mat24cols_ref, zn32_vec_i32_mat32cols_ref}; - -/** @brief applies a vmp product (int32_t* input) */ -EXPORT void default_zn32_vmp_apply_i32_ref(const MOD_Z* module, // - int32_t* res, uint64_t res_size, // - const int32_t* a, uint64_t a_size, // - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols) { - const uint64_t rows = a_size < nrows ? a_size : nrows; - const uint64_t cols = res_size < ncols ? res_size : ncols; - const uint64_t ncolblk = cols >> 5; - const uint64_t ncolrem = cols & 31; - // copy the first full blocks - const uint32_t full_blk_size = nrows * 32; - const int32_t* mat = (int32_t*)pmat; - int32_t* rr = res; - for (uint64_t blk = 0; // - blk < ncolblk; // - ++blk, mat += full_blk_size, rr += 32) { - zn32_vec_i32_mat32cols_ref(rows, rr, a, mat, 32); - } - // last block - if (ncolrem) { - uint64_t orig_rem = ncols - (ncolblk << 5); - uint64_t b_sl = orig_rem >= 32 ? 32 : orig_rem; - int32_t tmp[32]; - zn32_vec_i32_mat8kcols_ref[(ncolrem - 1) >> 3](rows, tmp, a, mat, b_sl); - memcpy(rr, tmp, ncolrem * sizeof(int32_t)); - } - // trailing bytes - memset(res + cols, 0, (res_size - cols) * sizeof(int32_t)); -} - -#endif
D spqlios/lib/spqlios/arithmetic/znx_small.c

@@ -1,38 +0,0 @@

-#include "vec_znx_arithmetic_private.h" - -/** @brief res = a * b : small integer polynomial product */ -EXPORT void fft64_znx_small_single_product(const MODULE* module, // N - int64_t* res, // output - const int64_t* a, // a - const int64_t* b, // b - uint8_t* tmp) { - const uint64_t nn = module->nn; - double* const ffta = (double*)tmp; - double* const fftb = ((double*)tmp) + nn; - reim_from_znx64(module->mod.fft64.p_conv, ffta, a); - reim_from_znx64(module->mod.fft64.p_conv, fftb, b); - reim_fft(module->mod.fft64.p_fft, ffta); - reim_fft(module->mod.fft64.p_fft, fftb); - reim_fftvec_mul_simple(module->m, ffta, ffta, fftb); - reim_ifft(module->mod.fft64.p_ifft, ffta); - reim_to_znx64(module->mod.fft64.p_reim_to_znx, res, ffta); -} - -/** @brief tmp bytes required for znx_small_single_product */ -EXPORT uint64_t fft64_znx_small_single_product_tmp_bytes(const MODULE* module) { - return 2 * module->nn * sizeof(double); -} - -/** @brief res = a * b : small integer polynomial product */ -EXPORT void znx_small_single_product(const MODULE* module, // N - int64_t* res, // output - const int64_t* a, // a - const int64_t* b, // b - uint8_t* tmp) { - module->func.znx_small_single_product(module, res, a, b, tmp); -} - -/** @brief tmp bytes required for znx_small_single_product */ -EXPORT uint64_t znx_small_single_product_tmp_bytes(const MODULE* module) { - return module->func.znx_small_single_product_tmp_bytes(module); -}
D spqlios/lib/spqlios/coeffs/coeffs_arithmetic.c

@@ -1,496 +0,0 @@

-#include "coeffs_arithmetic.h" - -#include <memory.h> -#include <assert.h> - -/** res = a + b */ -EXPORT void znx_add_i64_ref(uint64_t nn, int64_t* res, const int64_t* a, const int64_t* b) { - for (uint64_t i = 0; i < nn; ++i) { - res[i] = a[i] + b[i]; - } -} -/** res = a - b */ -EXPORT void znx_sub_i64_ref(uint64_t nn, int64_t* res, const int64_t* a, const int64_t* b) { - for (uint64_t i = 0; i < nn; ++i) { - res[i] = a[i] - b[i]; - } -} - -EXPORT void znx_negate_i64_ref(uint64_t nn, int64_t* res, const int64_t* a) { - for (uint64_t i = 0; i < nn; ++i) { - res[i] = -a[i]; - } -} -EXPORT void znx_copy_i64_ref(uint64_t nn, int64_t* res, const int64_t* a) { memcpy(res, a, nn * sizeof(int64_t)); } - -EXPORT void znx_zero_i64_ref(uint64_t nn, int64_t* res) { memset(res, 0, nn * sizeof(int64_t)); } - -EXPORT void rnx_divide_by_m_ref(uint64_t n, double m, double* res, const double* a) { - const double invm = 1. / m; - for (uint64_t i = 0; i < n; ++i) { - res[i] = a[i] * invm; - } -} - -EXPORT void rnx_rotate_f64(uint64_t nn, int64_t p, double* res, const double* in) { - uint64_t a = (-p) & (2 * nn - 1); // a= (-p) (pos)mod (2*nn) - - if (a < nn) { // rotate to the left - uint64_t nma = nn - a; - // rotate first half - for (uint64_t j = 0; j < nma; j++) { - res[j] = in[j + a]; - } - for (uint64_t j = nma; j < nn; j++) { - res[j] = -in[j - nma]; - } - } else { - a -= nn; - uint64_t nma = nn - a; - for (uint64_t j = 0; j < nma; j++) { - res[j] = -in[j + a]; - } - for (uint64_t j = nma; j < nn; j++) { - // rotate first half - res[j] = in[j - nma]; - } - } -} - -EXPORT void znx_rotate_i64(uint64_t nn, int64_t p, int64_t* res, const int64_t* in) { - uint64_t a = (-p) & (2 * nn - 1); // a= (-p) (pos)mod (2*nn) - - if (a < nn) { // rotate to the left - uint64_t nma = nn - a; - // rotate first half - for (uint64_t j = 0; j < nma; j++) { - res[j] = in[j + a]; - } - for (uint64_t j = nma; j < nn; j++) { - res[j] = -in[j - nma]; - } - } else { - a -= nn; - uint64_t nma = nn - a; - for (uint64_t j = 0; j < nma; j++) { - res[j] = -in[j + a]; - } - for (uint64_t j = nma; j < nn; j++) { - // rotate first half - res[j] = in[j - nma]; - } - } -} - -EXPORT void rnx_mul_xp_minus_one(uint64_t nn, int64_t p, double* res, const double* in) { - uint64_t a = (-p) & (2 * nn - 1); // a= (-p) (pos)mod (2*nn) - if (a < nn) { // rotate to the left - uint64_t nma = nn - a; - // rotate first half - for (uint64_t j = 0; j < nma; j++) { - res[j] = in[j + a] - in[j]; - } - for (uint64_t j = nma; j < nn; j++) { - res[j] = -in[j - nma] - in[j]; - } - } else { - a -= nn; - uint64_t nma = nn - a; - for (uint64_t j = 0; j < nma; j++) { - res[j] = -in[j + a] - in[j]; - } - for (uint64_t j = nma; j < nn; j++) { - // rotate first half - res[j] = in[j - nma] - in[j]; - } - } -} - -EXPORT void znx_mul_xp_minus_one(uint64_t nn, int64_t p, int64_t* res, const int64_t* in) { - uint64_t a = (-p) & (2 * nn - 1); // a= (-p) (pos)mod (2*nn) - if (a < nn) { // rotate to the left - uint64_t nma = nn - a; - // rotate first half - for (uint64_t j = 0; j < nma; j++) { - res[j] = in[j + a] - in[j]; - } - for (uint64_t j = nma; j < nn; j++) { - res[j] = -in[j - nma] - in[j]; - } - } else { - a -= nn; - uint64_t nma = nn - a; - for (uint64_t j = 0; j < nma; j++) { - res[j] = -in[j + a] - in[j]; - } - for (uint64_t j = nma; j < nn; j++) { - // rotate first half - res[j] = in[j - nma] - in[j]; - } - } -} - -// 0 < p < 2nn -EXPORT void rnx_automorphism_f64(uint64_t nn, int64_t p, double* res, const double* in) { - res[0] = in[0]; - uint64_t a = 0; - uint64_t _2mn = 2 * nn - 1; - for (uint64_t i = 1; i < nn; i++) { - a = (a + p) & _2mn; // i*p mod 2n - if (a < nn) { - res[a] = in[i]; // res[ip mod 2n] = res[i] - } else { - res[a - nn] = -in[i]; - } - } -} - -EXPORT void znx_automorphism_i64(uint64_t nn, int64_t p, int64_t* res, const int64_t* in) { - res[0] = in[0]; - uint64_t a = 0; - uint64_t _2mn = 2 * nn - 1; - for (uint64_t i = 1; i < nn; i++) { - a = (a + p) & _2mn; - if (a < nn) { - res[a] = in[i]; // res[ip mod 2n] = res[i] - } else { - res[a - nn] = -in[i]; - } - } -} - -EXPORT void rnx_rotate_inplace_f64(uint64_t nn, int64_t p, double* res) { - const uint64_t _2mn = 2 * nn - 1; - const uint64_t _mn = nn - 1; - uint64_t nb_modif = 0; - uint64_t j_start = 0; - while (nb_modif < nn) { - // follow the cycle that start with j_start - uint64_t j = j_start; - double tmp1 = res[j]; - do { - // find where the value should go, and with which sign - uint64_t new_j = (j + p) & _2mn; // mod 2n to get the position and sign - uint64_t new_j_n = new_j & _mn; // mod n to get just the position - // exchange this position with tmp1 (and take care of the sign) - double tmp2 = res[new_j_n]; - res[new_j_n] = (new_j < nn) ? tmp1 : -tmp1; - tmp1 = tmp2; - // move to the new location, and store the number of items modified - ++nb_modif; - j = new_j_n; - } while (j != j_start); - // move to the start of the next cycle: - // we need to find an index that has not been touched yet, and pick it as next j_start. - // in practice, it is enough to do +1, because the group of rotations is cyclic and 1 is a generator. - ++j_start; - } -} - -EXPORT void znx_rotate_inplace_i64(uint64_t nn, int64_t p, int64_t* res) { - const uint64_t _2mn = 2 * nn - 1; - const uint64_t _mn = nn - 1; - uint64_t nb_modif = 0; - uint64_t j_start = 0; - while (nb_modif < nn) { - // follow the cycle that start with j_start - uint64_t j = j_start; - int64_t tmp1 = res[j]; - do { - // find where the value should go, and with which sign - uint64_t new_j = (j + p) & _2mn; // mod 2n to get the position and sign - uint64_t new_j_n = new_j & _mn; // mod n to get just the position - // exchange this position with tmp1 (and take care of the sign) - int64_t tmp2 = res[new_j_n]; - res[new_j_n] = (new_j < nn) ? tmp1 : -tmp1; - tmp1 = tmp2; - // move to the new location, and store the number of items modified - ++nb_modif; - j = new_j_n; - } while (j != j_start); - // move to the start of the next cycle: - // we need to find an index that has not been touched yet, and pick it as next j_start. - // in practice, it is enough to do +1, because the group of rotations is cyclic and 1 is a generator. - ++j_start; - } -} - -EXPORT void rnx_mul_xp_minus_one_inplace(uint64_t nn, int64_t p, double* res) { - const uint64_t _2mn = 2 * nn - 1; - const uint64_t _mn = nn - 1; - uint64_t nb_modif = 0; - uint64_t j_start = 0; - while (nb_modif < nn) { - // follow the cycle that start with j_start - uint64_t j = j_start; - double tmp1 = res[j]; - do { - // find where the value should go, and with which sign - uint64_t new_j = (j + p) & _2mn; // mod 2n to get the position and sign - uint64_t new_j_n = new_j & _mn; // mod n to get just the position - // exchange this position with tmp1 (and take care of the sign) - double tmp2 = res[new_j_n]; - res[new_j_n] = ((new_j < nn) ? tmp1 : -tmp1) - res[new_j_n]; - tmp1 = tmp2; - // move to the new location, and store the number of items modified - ++nb_modif; - j = new_j_n; - } while (j != j_start); - // move to the start of the next cycle: - // we need to find an index that has not been touched yet, and pick it as next j_start. - // in practice, it is enough to do +1, because the group of rotations is cyclic and 1 is a generator. - ++j_start; - } -} - -__always_inline int64_t get_base_k_digit(const int64_t x, const uint64_t base_k) { - return (x << (64 - base_k)) >> (64 - base_k); -} - -__always_inline int64_t get_base_k_carry(const int64_t x, const int64_t digit, const uint64_t base_k) { - return (x - digit) >> base_k; -} - -EXPORT void znx_normalize(uint64_t nn, uint64_t base_k, int64_t* out, int64_t* carry_out, const int64_t* in, - const int64_t* carry_in) { - assert(in); - if (out != 0) { - if (carry_in != 0x0 && carry_out != 0x0) { - // with carry in and carry out is computed - for (uint64_t i = 0; i < nn; ++i) { - const int64_t x = in[i]; - const int64_t cin = carry_in[i]; - - int64_t digit = get_base_k_digit(x, base_k); - int64_t carry = get_base_k_carry(x, digit, base_k); - int64_t digit_plus_cin = digit + cin; - int64_t y = get_base_k_digit(digit_plus_cin, base_k); - int64_t cout = carry + get_base_k_carry(digit_plus_cin, y, base_k); - - out[i] = y; - carry_out[i] = cout; - } - } else if (carry_in != 0) { - // with carry in and carry out is dropped - for (uint64_t i = 0; i < nn; ++i) { - const int64_t x = in[i]; - const int64_t cin = carry_in[i]; - - int64_t digit = get_base_k_digit(x, base_k); - int64_t digit_plus_cin = digit + cin; - int64_t y = get_base_k_digit(digit_plus_cin, base_k); - - out[i] = y; - } - - } else if (carry_out != 0) { - // no carry in and carry out is computed - for (uint64_t i = 0; i < nn; ++i) { - const int64_t x = in[i]; - - int64_t y = get_base_k_digit(x, base_k); - int64_t cout = get_base_k_carry(x, y, base_k); - - out[i] = y; - carry_out[i] = cout; - } - - } else { - // no carry in and carry out is dropped - for (uint64_t i = 0; i < nn; ++i) { - out[i] = get_base_k_digit(in[i], base_k); - } - } - } else { - assert(carry_out); - if (carry_in != 0x0) { - // with carry in and carry out is computed - for (uint64_t i = 0; i < nn; ++i) { - const int64_t x = in[i]; - const int64_t cin = carry_in[i]; - - int64_t digit = get_base_k_digit(x, base_k); - int64_t carry = get_base_k_carry(x, digit, base_k); - int64_t digit_plus_cin = digit + cin; - int64_t y = get_base_k_digit(digit_plus_cin, base_k); - int64_t cout = carry + get_base_k_carry(digit_plus_cin, y, base_k); - - carry_out[i] = cout; - } - } else { - // no carry in and carry out is computed - for (uint64_t i = 0; i < nn; ++i) { - const int64_t x = in[i]; - - int64_t y = get_base_k_digit(x, base_k); - int64_t cout = get_base_k_carry(x, y, base_k); - - carry_out[i] = cout; - } - } - } -} - -void znx_automorphism_inplace_i64(uint64_t nn, int64_t p, int64_t* res) { - const uint64_t _2mn = 2 * nn - 1; - const uint64_t _mn = nn - 1; - const uint64_t m = nn >> 1; - // reduce p mod 2n - p &= _2mn; - // uint64_t vp = p & _2mn; - /// uint64_t target_modifs = m >> 1; - // we proceed by increasing binary valuation - for (uint64_t binval = 1, vp = p & _2mn, orb_size = m; binval < nn; - binval <<= 1, vp = (vp << 1) & _2mn, orb_size >>= 1) { - // In this loop, we are going to treat the orbit of indexes = binval mod 2.binval. - // At the beginning of this loop we have: - // vp = binval * p mod 2n - // target_modif = m / binval (i.e. order of the orbit binval % 2.binval) - - // first, handle the orders 1 and 2. - // if p*binval == binval % 2n: we're done! - if (vp == binval) return; - // if p*binval == -binval % 2n: nega-mirror the orbit and all the sub-orbits and exit! - if (((vp + binval) & _2mn) == 0) { - for (uint64_t j = binval; j < m; j += binval) { - int64_t tmp = res[j]; - res[j] = -res[nn - j]; - res[nn - j] = -tmp; - } - res[m] = -res[m]; - return; - } - // if p*binval == binval + n % 2n: negate the orbit and exit - if (((vp - binval) & _mn) == 0) { - for (uint64_t j = binval; j < nn; j += 2 * binval) { - res[j] = -res[j]; - } - return; - } - // if p*binval == n - binval % 2n: mirror the orbit and continue! - if (((vp + binval) & _mn) == 0) { - for (uint64_t j = binval; j < m; j += 2 * binval) { - int64_t tmp = res[j]; - res[j] = res[nn - j]; - res[nn - j] = tmp; - } - continue; - } - // otherwise we will follow the orbit cycles, - // starting from binval and -binval in parallel - uint64_t j_start = binval; - uint64_t nb_modif = 0; - while (nb_modif < orb_size) { - // follow the cycle that start with j_start - uint64_t j = j_start; - int64_t tmp1 = res[j]; - int64_t tmp2 = res[nn - j]; - do { - // find where the value should go, and with which sign - uint64_t new_j = (j * p) & _2mn; // mod 2n to get the position and sign - uint64_t new_j_n = new_j & _mn; // mod n to get just the position - // exchange this position with tmp1 (and take care of the sign) - int64_t tmp1a = res[new_j_n]; - int64_t tmp2a = res[nn - new_j_n]; - if (new_j < nn) { - res[new_j_n] = tmp1; - res[nn - new_j_n] = tmp2; - } else { - res[new_j_n] = -tmp1; - res[nn - new_j_n] = -tmp2; - } - tmp1 = tmp1a; - tmp2 = tmp2a; - // move to the new location, and store the number of items modified - nb_modif += 2; - j = new_j_n; - } while (j != j_start); - // move to the start of the next cycle: - // we need to find an index that has not been touched yet, and pick it as next j_start. - // in practice, it is enough to do *5, because 5 is a generator. - j_start = (5 * j_start) & _mn; - } - } -} - -void rnx_automorphism_inplace_f64(uint64_t nn, int64_t p, double* res) { - const uint64_t _2mn = 2 * nn - 1; - const uint64_t _mn = nn - 1; - const uint64_t m = nn >> 1; - // reduce p mod 2n - p &= _2mn; - // uint64_t vp = p & _2mn; - /// uint64_t target_modifs = m >> 1; - // we proceed by increasing binary valuation - for (uint64_t binval = 1, vp = p & _2mn, orb_size = m; binval < nn; - binval <<= 1, vp = (vp << 1) & _2mn, orb_size >>= 1) { - // In this loop, we are going to treat the orbit of indexes = binval mod 2.binval. - // At the beginning of this loop we have: - // vp = binval * p mod 2n - // target_modif = m / binval (i.e. order of the orbit binval % 2.binval) - - // first, handle the orders 1 and 2. - // if p*binval == binval % 2n: we're done! - if (vp == binval) return; - // if p*binval == -binval % 2n: nega-mirror the orbit and all the sub-orbits and exit! - if (((vp + binval) & _2mn) == 0) { - for (uint64_t j = binval; j < m; j += binval) { - double tmp = res[j]; - res[j] = -res[nn - j]; - res[nn - j] = -tmp; - } - res[m] = -res[m]; - return; - } - // if p*binval == binval + n % 2n: negate the orbit and exit - if (((vp - binval) & _mn) == 0) { - for (uint64_t j = binval; j < nn; j += 2 * binval) { - res[j] = -res[j]; - } - return; - } - // if p*binval == n - binval % 2n: mirror the orbit and continue! - if (((vp + binval) & _mn) == 0) { - for (uint64_t j = binval; j < m; j += 2 * binval) { - double tmp = res[j]; - res[j] = res[nn - j]; - res[nn - j] = tmp; - } - continue; - } - // otherwise we will follow the orbit cycles, - // starting from binval and -binval in parallel - uint64_t j_start = binval; - uint64_t nb_modif = 0; - while (nb_modif < orb_size) { - // follow the cycle that start with j_start - uint64_t j = j_start; - double tmp1 = res[j]; - double tmp2 = res[nn - j]; - do { - // find where the value should go, and with which sign - uint64_t new_j = (j * p) & _2mn; // mod 2n to get the position and sign - uint64_t new_j_n = new_j & _mn; // mod n to get just the position - // exchange this position with tmp1 (and take care of the sign) - double tmp1a = res[new_j_n]; - double tmp2a = res[nn - new_j_n]; - if (new_j < nn) { - res[new_j_n] = tmp1; - res[nn - new_j_n] = tmp2; - } else { - res[new_j_n] = -tmp1; - res[nn - new_j_n] = -tmp2; - } - tmp1 = tmp1a; - tmp2 = tmp2a; - // move to the new location, and store the number of items modified - nb_modif += 2; - j = new_j_n; - } while (j != j_start); - // move to the start of the next cycle: - // we need to find an index that has not been touched yet, and pick it as next j_start. - // in practice, it is enough to do *5, because 5 is a generator. - j_start = (5 * j_start) & _mn; - } - } -}
D spqlios/lib/spqlios/coeffs/coeffs_arithmetic.h

@@ -1,78 +0,0 @@

-#ifndef SPQLIOS_COEFFS_ARITHMETIC_H -#define SPQLIOS_COEFFS_ARITHMETIC_H - -#include "../commons.h" - -/** res = a + b */ -EXPORT void znx_add_i64_ref(uint64_t nn, int64_t* res, const int64_t* a, const int64_t* b); -EXPORT void znx_add_i64_avx(uint64_t nn, int64_t* res, const int64_t* a, const int64_t* b); -/** res = a - b */ -EXPORT void znx_sub_i64_ref(uint64_t nn, int64_t* res, const int64_t* a, const int64_t* b); -EXPORT void znx_sub_i64_avx(uint64_t nn, int64_t* res, const int64_t* a, const int64_t* b); -/** res = -a */ -EXPORT void znx_negate_i64_ref(uint64_t nn, int64_t* res, const int64_t* a); -EXPORT void znx_negate_i64_avx(uint64_t nn, int64_t* res, const int64_t* a); -/** res = a */ -EXPORT void znx_copy_i64_ref(uint64_t nn, int64_t* res, const int64_t* a); -/** res = 0 */ -EXPORT void znx_zero_i64_ref(uint64_t nn, int64_t* res); - -/** res = a / m where m is a power of 2 */ -EXPORT void rnx_divide_by_m_ref(uint64_t nn, double m, double* res, const double* a); -EXPORT void rnx_divide_by_m_avx(uint64_t nn, double m, double* res, const double* a); - -/** - * @param res = X^p *in mod X^nn +1 - * @param nn the ring dimension - * @param p a power for the rotation -2nn <= p <= 2nn - * @param in is a rnx/znx vector of dimension nn - */ -EXPORT void rnx_rotate_f64(uint64_t nn, int64_t p, double* res, const double* in); -EXPORT void znx_rotate_i64(uint64_t nn, int64_t p, int64_t* res, const int64_t* in); -EXPORT void rnx_rotate_inplace_f64(uint64_t nn, int64_t p, double* res); -EXPORT void znx_rotate_inplace_i64(uint64_t nn, int64_t p, int64_t* res); - -/** - * @brief res(X) = in(X^p) - * @param nn the ring dimension - * @param p is odd integer and must be between 0 < p < 2nn - * @param in is a rnx/znx vector of dimension nn - */ -EXPORT void rnx_automorphism_f64(uint64_t nn, int64_t p, double* res, const double* in); -EXPORT void znx_automorphism_i64(uint64_t nn, int64_t p, int64_t* res, const int64_t* in); -EXPORT void rnx_automorphism_inplace_f64(uint64_t nn, int64_t p, double* res); -EXPORT void znx_automorphism_inplace_i64(uint64_t nn, int64_t p, int64_t* res); - -/** - * @brief res = (X^p-1).in - * @param nn the ring dimension - * @param p must be between -2nn <= p <= 2nn - * @param in is a rnx/znx vector of dimension nn - */ -EXPORT void rnx_mul_xp_minus_one(uint64_t nn, int64_t p, double* res, const double* in); -EXPORT void znx_mul_xp_minus_one(uint64_t nn, int64_t p, int64_t* res, const int64_t* in); -EXPORT void rnx_mul_xp_minus_one_inplace(uint64_t nn, int64_t p, double* res); - -/** - * @brief Normalize input plus carry mod-2^k. The following - * equality holds @c {in + carry_in == out + carry_out . 2^k}. - * - * @c in must be in [-2^62 .. 2^62] - * - * @c out is in [ -2^(base_k-1), 2^(base_k-1) [. - * - * @c carry_in and @carry_out have at most 64+1-k bits. - * - * Null @c carry_in or @c carry_out are ignored. - * - * @param[in] nn the ring dimension - * @param[in] base_k the base k - * @param out output normalized znx - * @param carry_out output carry znx - * @param[in] in input znx - * @param[in] carry_in input carry znx - */ -EXPORT void znx_normalize(uint64_t nn, uint64_t base_k, int64_t* out, int64_t* carry_out, const int64_t* in, - const int64_t* carry_in); - -#endif // SPQLIOS_COEFFS_ARITHMETIC_H
D spqlios/lib/spqlios/coeffs/coeffs_arithmetic_avx.c

@@ -1,124 +0,0 @@

-#include <immintrin.h> - -#include "../commons_private.h" -#include "coeffs_arithmetic.h" - -// res = a + b. dimension n must be a power of 2 -EXPORT void znx_add_i64_avx(uint64_t nn, int64_t* res, const int64_t* a, const int64_t* b) { - if (nn <= 2) { - if (nn == 1) { - res[0] = a[0] + b[0]; - } else { - _mm_storeu_si128((__m128i*)res, // - _mm_add_epi64( // - _mm_loadu_si128((__m128i*)a), // - _mm_loadu_si128((__m128i*)b))); - } - } else { - const __m256i* aa = (__m256i*)a; - const __m256i* bb = (__m256i*)b; - __m256i* rr = (__m256i*)res; - __m256i* const rrend = (__m256i*)(res + nn); - do { - _mm256_storeu_si256(rr, // - _mm256_add_epi64( // - _mm256_loadu_si256(aa), // - _mm256_loadu_si256(bb))); - ++rr; - ++aa; - ++bb; - } while (rr < rrend); - } -} - -// res = a - b. dimension n must be a power of 2 -EXPORT void znx_sub_i64_avx(uint64_t nn, int64_t* res, const int64_t* a, const int64_t* b) { - if (nn <= 2) { - if (nn == 1) { - res[0] = a[0] - b[0]; - } else { - _mm_storeu_si128((__m128i*)res, // - _mm_sub_epi64( // - _mm_loadu_si128((__m128i*)a), // - _mm_loadu_si128((__m128i*)b))); - } - } else { - const __m256i* aa = (__m256i*)a; - const __m256i* bb = (__m256i*)b; - __m256i* rr = (__m256i*)res; - __m256i* const rrend = (__m256i*)(res + nn); - do { - _mm256_storeu_si256(rr, // - _mm256_sub_epi64( // - _mm256_loadu_si256(aa), // - _mm256_loadu_si256(bb))); - ++rr; - ++aa; - ++bb; - } while (rr < rrend); - } -} - -EXPORT void znx_negate_i64_avx(uint64_t nn, int64_t* res, const int64_t* a) { - if (nn <= 2) { - if (nn == 1) { - res[0] = -a[0]; - } else { - _mm_storeu_si128((__m128i*)res, // - _mm_sub_epi64( // - _mm_set1_epi64x(0), // - _mm_loadu_si128((__m128i*)a))); - } - } else { - const __m256i* aa = (__m256i*)a; - __m256i* rr = (__m256i*)res; - __m256i* const rrend = (__m256i*)(res + nn); - do { - _mm256_storeu_si256(rr, // - _mm256_sub_epi64( // - _mm256_set1_epi64x(0), // - _mm256_loadu_si256(aa))); - ++rr; - ++aa; - } while (rr < rrend); - } -} - -EXPORT void rnx_divide_by_m_avx(uint64_t n, double m, double* res, const double* a) { - // TODO: see if there is a faster way of dividing by a power of 2? - const double invm = 1. / m; - if (n < 8) { - switch (n) { - case 1: - *res = *a * invm; - break; - case 2: - _mm_storeu_pd(res, // - _mm_mul_pd(_mm_loadu_pd(a), // - _mm_set1_pd(invm))); - break; - case 4: - _mm256_storeu_pd(res, // - _mm256_mul_pd(_mm256_loadu_pd(a), // - _mm256_set1_pd(invm))); - break; - default: - NOT_SUPPORTED(); // non-power of 2 - } - return; - } - const __m256d invm256 = _mm256_set1_pd(invm); - double* rr = res; - const double* aa = a; - const double* const aaend = a + n; - do { - _mm256_storeu_pd(rr, // - _mm256_mul_pd(_mm256_loadu_pd(aa), // - invm256)); - _mm256_storeu_pd(rr + 4, // - _mm256_mul_pd(_mm256_loadu_pd(aa + 4), // - invm256)); - rr += 8; - aa += 8; - } while (aa < aaend); -}
D spqlios/lib/spqlios/commons.c

@@ -1,165 +0,0 @@

-#include "commons.h" - -#include <math.h> -#include <stdio.h> -#include <stdlib.h> - -EXPORT void* UNDEFINED_p_ii(int32_t n, int32_t m) { UNDEFINED(); } -EXPORT void* UNDEFINED_p_uu(uint32_t n, uint32_t m) { UNDEFINED(); } -EXPORT double* UNDEFINED_dp_pi(const void* p, int32_t n) { UNDEFINED(); } -EXPORT void* UNDEFINED_vp_pi(const void* p, int32_t n) { UNDEFINED(); } -EXPORT void* UNDEFINED_vp_pu(const void* p, uint32_t n) { UNDEFINED(); } -EXPORT void UNDEFINED_v_vpdp(const void* p, double* a) { UNDEFINED(); } -EXPORT void UNDEFINED_v_vpvp(const void* p, void* a) { UNDEFINED(); } -EXPORT double* NOT_IMPLEMENTED_dp_i(int32_t n) { NOT_IMPLEMENTED(); } -EXPORT void* NOT_IMPLEMENTED_vp_i(int32_t n) { NOT_IMPLEMENTED(); } -EXPORT void* NOT_IMPLEMENTED_vp_u(uint32_t n) { NOT_IMPLEMENTED(); } -EXPORT void NOT_IMPLEMENTED_v_dp(double* a) { NOT_IMPLEMENTED(); } -EXPORT void NOT_IMPLEMENTED_v_vp(void* p) { NOT_IMPLEMENTED(); } -EXPORT void NOT_IMPLEMENTED_v_idpdpdp(int32_t n, double* a, const double* b, const double* c) { NOT_IMPLEMENTED(); } -EXPORT void NOT_IMPLEMENTED_v_uvpcvpcvp(uint32_t n, void* r, const void* a, const void* b) { NOT_IMPLEMENTED(); } -EXPORT void NOT_IMPLEMENTED_v_uvpvpcvp(uint32_t n, void* a, void* b, const void* o) { NOT_IMPLEMENTED(); } - -#ifdef _WIN32 -#define __always_inline inline __attribute((always_inline)) -#endif - -void internal_accurate_sincos(double* rcos, double* rsin, double x) { - double _4_x_over_pi = 4 * x / M_PI; - int64_t int_part = ((int64_t)rint(_4_x_over_pi)) & 7; - double frac_part = _4_x_over_pi - (double)(int_part); - double frac_x = M_PI * frac_part / 4.; - // compute the taylor series - double cosp = 1.; - double sinp = 0.; - double powx = 1.; - int64_t nn = 0; - while (fabs(powx) > 1e-20) { - ++nn; - powx = powx * frac_x / (double)(nn); // x^n/n! - switch (nn & 3) { - case 0: - cosp += powx; - break; - case 1: - sinp += powx; - break; - case 2: - cosp -= powx; - break; - case 3: - sinp -= powx; - break; - default: - abort(); // impossible - } - } - // final multiplication - switch (int_part) { - case 0: - *rcos = cosp; - *rsin = sinp; - break; - case 1: - *rcos = M_SQRT1_2 * (cosp - sinp); - *rsin = M_SQRT1_2 * (cosp + sinp); - break; - case 2: - *rcos = -sinp; - *rsin = cosp; - break; - case 3: - *rcos = -M_SQRT1_2 * (cosp + sinp); - *rsin = M_SQRT1_2 * (cosp - sinp); - break; - case 4: - *rcos = -cosp; - *rsin = -sinp; - break; - case 5: - *rcos = -M_SQRT1_2 * (cosp - sinp); - *rsin = -M_SQRT1_2 * (cosp + sinp); - break; - case 6: - *rcos = sinp; - *rsin = -cosp; - break; - case 7: - *rcos = M_SQRT1_2 * (cosp + sinp); - *rsin = -M_SQRT1_2 * (cosp - sinp); - break; - default: - abort(); // impossible - } - if (fabs(cos(x) - *rcos) > 1e-10 || fabs(sin(x) - *rsin) > 1e-10) { - printf("cos(%.17lf) =? %.17lf instead of %.17lf\n", x, *rcos, cos(x)); - printf("sin(%.17lf) =? %.17lf instead of %.17lf\n", x, *rsin, sin(x)); - printf("fracx = %.17lf\n", frac_x); - printf("cosp = %.17lf\n", cosp); - printf("sinp = %.17lf\n", sinp); - printf("nn = %d\n", (int)(nn)); - } -} - -double internal_accurate_cos(double x) { - double rcos, rsin; - internal_accurate_sincos(&rcos, &rsin, x); - return rcos; -} -double internal_accurate_sin(double x) { - double rcos, rsin; - internal_accurate_sincos(&rcos, &rsin, x); - return rsin; -} - -EXPORT void spqlios_debug_free(void* addr) { free((uint8_t*)addr - 64); } - -EXPORT void* spqlios_debug_alloc(uint64_t size) { return (uint8_t*)malloc(size + 64) + 64; } - -EXPORT void spqlios_free(void* addr) { -#ifndef NDEBUG - // in debug mode, we deallocated with spqlios_debug_free() - spqlios_debug_free(addr); -#else - // in release mode, the function will free aligned memory -#ifdef _WIN32 - _aligned_free(addr); -#else - free(addr); -#endif -#endif -} - -EXPORT void* spqlios_alloc(uint64_t size) { -#ifndef NDEBUG - // in debug mode, the function will not necessarily have any particular alignment - // it will also ensure that memory can only be deallocated with spqlios_free() - return spqlios_debug_alloc(size); -#else - // in release mode, the function will return 64-bytes aligned memory -#ifdef _WIN32 - void* reps = _aligned_malloc((size + 63) & (UINT64_C(-64)), 64); -#else - void* reps = aligned_alloc(64, (size + 63) & (UINT64_C(-64))); -#endif - if (reps == 0) FATAL_ERROR("Out of memory"); - return reps; -#endif -} - -EXPORT void* spqlios_alloc_custom_align(uint64_t align, uint64_t size) { -#ifndef NDEBUG - // in debug mode, the function will not necessarily have any particular alignment - // it will also ensure that memory can only be deallocated with spqlios_free() - return spqlios_debug_alloc(size); -#else - // in release mode, the function will return aligned memory -#ifdef _WIN32 - void* reps = _aligned_malloc(size, align); -#else - void* reps = aligned_alloc(align, size); -#endif - if (reps == 0) FATAL_ERROR("Out of memory"); - return reps; -#endif -}
D spqlios/lib/spqlios/commons.h

@@ -1,77 +0,0 @@

-#ifndef SPQLIOS_COMMONS_H -#define SPQLIOS_COMMONS_H - -#ifdef __cplusplus -#include <cstdint> -#include <cstdio> -#include <cstdlib> -#define EXPORT extern "C" -#define EXPORT_DECL extern "C" -#else -#include <stdint.h> -#include <stdio.h> -#include <stdlib.h> -#define EXPORT -#define EXPORT_DECL extern -#define nullptr 0x0; -#endif - -#define UNDEFINED() \ - { \ - fprintf(stderr, "UNDEFINED!!!\n"); \ - abort(); \ - } -#define NOT_IMPLEMENTED() \ - { \ - fprintf(stderr, "NOT IMPLEMENTED!!!\n"); \ - abort(); \ - } -#define FATAL_ERROR(MESSAGE) \ - { \ - fprintf(stderr, "ERROR: %s\n", (MESSAGE)); \ - abort(); \ - } - -EXPORT void* UNDEFINED_p_ii(int32_t n, int32_t m); -EXPORT void* UNDEFINED_p_uu(uint32_t n, uint32_t m); -EXPORT double* UNDEFINED_dp_pi(const void* p, int32_t n); -EXPORT void* UNDEFINED_vp_pi(const void* p, int32_t n); -EXPORT void* UNDEFINED_vp_pu(const void* p, uint32_t n); -EXPORT void UNDEFINED_v_vpdp(const void* p, double* a); -EXPORT void UNDEFINED_v_vpvp(const void* p, void* a); -EXPORT double* NOT_IMPLEMENTED_dp_i(int32_t n); -EXPORT void* NOT_IMPLEMENTED_vp_i(int32_t n); -EXPORT void* NOT_IMPLEMENTED_vp_u(uint32_t n); -EXPORT void NOT_IMPLEMENTED_v_dp(double* a); -EXPORT void NOT_IMPLEMENTED_v_vp(void* p); -EXPORT void NOT_IMPLEMENTED_v_idpdpdp(int32_t n, double* a, const double* b, const double* c); -EXPORT void NOT_IMPLEMENTED_v_uvpcvpcvp(uint32_t n, void* r, const void* a, const void* b); -EXPORT void NOT_IMPLEMENTED_v_uvpvpcvp(uint32_t n, void* a, void* b, const void* o); - -// windows - -#if defined(_WIN32) || defined(__APPLE__) -#define __always_inline inline __attribute((always_inline)) -#endif - -EXPORT void spqlios_free(void* address); - -EXPORT void* spqlios_alloc(uint64_t size); -EXPORT void* spqlios_alloc_custom_align(uint64_t align, uint64_t size); - -#define USE_LIBM_SIN_COS -#ifndef USE_LIBM_SIN_COS -// if at some point, we want to remove the libm dependency, we can -// consider this: -EXPORT double internal_accurate_cos(double x); -EXPORT double internal_accurate_sin(double x); -EXPORT void internal_accurate_sincos(double* rcos, double* rsin, double x); -#define m_accurate_cos internal_accurate_cos -#define m_accurate_sin internal_accurate_sin -#else -// let's use libm sin and cos -#define m_accurate_cos cos -#define m_accurate_sin sin -#endif - -#endif // SPQLIOS_COMMONS_H
D spqlios/lib/spqlios/commons_private.c

@@ -1,55 +0,0 @@

-#include "commons_private.h" - -#include <stdio.h> -#include <stdlib.h> - -#include "commons.h" - -EXPORT void* spqlios_error(const char* error) { - fputs(error, stderr); - abort(); - return nullptr; -} -EXPORT void* spqlios_keep_or_free(void* ptr, void* ptr2) { - if (!ptr2) { - free(ptr); - } - return ptr2; -} - -EXPORT uint32_t log2m(uint32_t m) { - uint32_t a = m - 1; - if (m & a) FATAL_ERROR("m must be a power of two"); - a = (a & 0x55555555u) + ((a >> 1) & 0x55555555u); - a = (a & 0x33333333u) + ((a >> 2) & 0x33333333u); - a = (a & 0x0F0F0F0Fu) + ((a >> 4) & 0x0F0F0F0Fu); - a = (a & 0x00FF00FFu) + ((a >> 8) & 0x00FF00FFu); - return (a & 0x0000FFFFu) + ((a >> 16) & 0x0000FFFFu); -} - -EXPORT uint64_t is_not_pow2_double(void* doublevalue) { return (*(uint64_t*)doublevalue) & 0x7FFFFFFFFFFFFUL; } - -uint32_t revbits(uint32_t nbits, uint32_t value) { - uint32_t res = 0; - for (uint32_t i = 0; i < nbits; ++i) { - res = (res << 1) + (value & 1); - value >>= 1; - } - return res; -} - -/** - * @brief this computes the sequence: 0,1/2,1/4,3/4,1/8,5/8,3/8,7/8,... - * essentially: the bits of (i+1) in lsb order on the basis (1/2^k) mod 1*/ -double fracrevbits(uint32_t i) { - if (i == 0) return 0; - if (i == 1) return 0.5; - if (i % 2 == 0) - return fracrevbits(i / 2) / 2.; - else - return fracrevbits((i - 1) / 2) / 2. + 0.5; -} - -uint64_t ceilto64b(uint64_t size) { return (size + UINT64_C(63)) & (UINT64_C(-64)); } - -uint64_t ceilto32b(uint64_t size) { return (size + UINT64_C(31)) & (UINT64_C(-32)); }
D spqlios/lib/spqlios/commons_private.h

@@ -1,72 +0,0 @@

-#ifndef SPQLIOS_COMMONS_PRIVATE_H -#define SPQLIOS_COMMONS_PRIVATE_H - -#include "commons.h" - -#ifdef __cplusplus -#include <cmath> -#include <cstdio> -#include <cstdlib> -#else -#include <math.h> -#include <stdio.h> -#include <stdlib.h> -#define nullptr 0x0; -#endif - -/** @brief log2 of a power of two (UB if m is not a power of two) */ -EXPORT uint32_t log2m(uint32_t m); - -/** @brief checks if the doublevalue is a power of two */ -EXPORT uint64_t is_not_pow2_double(void* doublevalue); - -#define UNDEFINED() \ - { \ - fprintf(stderr, "UNDEFINED!!!\n"); \ - abort(); \ - } -#define NOT_IMPLEMENTED() \ - { \ - fprintf(stderr, "NOT IMPLEMENTED!!!\n"); \ - abort(); \ - } -#define NOT_SUPPORTED() \ - { \ - fprintf(stderr, "NOT SUPPORTED!!!\n"); \ - abort(); \ - } -#define FATAL_ERROR(MESSAGE) \ - { \ - fprintf(stderr, "ERROR: %s\n", (MESSAGE)); \ - abort(); \ - } - -#define STATIC_ASSERT(condition) (void)sizeof(char[-1 + 2 * !!(condition)]) - -/** @brief reports the error and returns nullptr */ -EXPORT void* spqlios_error(const char* error); -/** @brief if ptr2 is not null, returns ptr, otherwise free ptr and return null */ -EXPORT void* spqlios_keep_or_free(void* ptr, void* ptr2); - -#ifdef __x86_64__ -#define CPU_SUPPORTS __builtin_cpu_supports -#else -// TODO for now, we do not have any optimization for non x86 targets -#define CPU_SUPPORTS(xxxx) 0 -#endif - -/** @brief returns the n bits of value in reversed order */ -EXPORT uint32_t revbits(uint32_t nbits, uint32_t value); - -/** - * @brief this computes the sequence: 0,1/2,1/4,3/4,1/8,5/8,3/8,7/8,... - * essentially: the bits of (i+1) in lsb order on the basis (1/2^k) mod 1*/ -EXPORT double fracrevbits(uint32_t i); - -/** @brief smallest multiple of 64 higher or equal to size */ -EXPORT uint64_t ceilto64b(uint64_t size); - -/** @brief smallest multiple of 32 higher or equal to size */ -EXPORT uint64_t ceilto32b(uint64_t size); - -#endif // SPQLIOS_COMMONS_PRIVATE_H
D spqlios/lib/spqlios/cplx/README.md

@@ -1,22 +0,0 @@

-In this folder, we deal with the full complex FFT in `C[X] mod X^M-i`. -One complex is represented by two consecutive doubles `(real,imag)` -Note that a real polynomial sum_{j=0}^{N-1} p_j.X^j mod X^N+1 -corresponds to the complex polynomial of half degree `M=N/2`: -`sum_{j=0}^{M-1} (p_{j} + i.p_{j+M}) X^j mod X^M-i` - -For a complex polynomial A(X) sum c_i X^i of degree M-1 -or a real polynomial sum a_i X^i of degree N - -coefficient space: -a_0,a_M,a_1,a_{M+1},...,a_{M-1},a_{2M-1} -or equivalently -Re(c_0),Im(c_0),Re(c_1),Im(c_1),...Re(c_{M-1}),Im(c_{M-1}) - -eval space: -c(omega_{0}),...,c(omega_{M-1}) - -where -omega_j = omega^{1+rev_{2N}(j)} -and omega = exp(i.pi/N) - -rev_{2N}(j) is the number that has the log2(2N) bits of j in reverse order.
D spqlios/lib/spqlios/cplx/cplx_common.c

@@ -1,80 +0,0 @@

-#include "cplx_fft_internal.h" - -void cplx_set(CPLX r, const CPLX a) { - r[0] = a[0]; - r[1] = a[1]; -} -void cplx_neg(CPLX r, const CPLX a) { - r[0] = -a[0]; - r[1] = -a[1]; -} -void cplx_add(CPLX r, const CPLX a, const CPLX b) { - r[0] = a[0] + b[0]; - r[1] = a[1] + b[1]; -} -void cplx_sub(CPLX r, const CPLX a, const CPLX b) { - r[0] = a[0] - b[0]; - r[1] = a[1] - b[1]; -} -void cplx_mul(CPLX r, const CPLX a, const CPLX b) { - double re = a[0] * b[0] - a[1] * b[1]; - r[1] = a[0] * b[1] + a[1] * b[0]; - r[0] = re; -} - -/** - * @brief splits 2h evaluations of one polynomials into 2 times h evaluations of even/odd polynomial - * Input: Q_0(y),...,Q_{h-1}(y),Q_0(-y),...,Q_{h-1}(-y) - * Output: P_0(z),...,P_{h-1}(z),P_h(z),...,P_{2h-1}(z) - * where Q_i(X)=P_i(X^2)+X.P_{h+i}(X^2) and y^2 = z - * @param h number of "coefficients" h >= 1 - * @param data 2h complex coefficients interleaved and 256b aligned - * @param powom y represented as (yre,yim) - */ -EXPORT void cplx_split_fft_ref(int32_t h, CPLX* data, const CPLX powom) { - CPLX* d0 = data; - CPLX* d1 = data + h; - for (uint64_t i = 0; i < h; ++i) { - CPLX diff; - cplx_sub(diff, d0[i], d1[i]); - cplx_add(d0[i], d0[i], d1[i]); - cplx_mul(d1[i], diff, powom); - } -} - -/** - * @brief Do two layers of itwiddle (i.e. split). - * Input/output: d0,d1,d2,d3 of length h - * Algo: - * itwiddle(d0,d1,om[0]),itwiddle(d2,d3,i.om[0]) - * itwiddle(d0,d2,om[1]),itwiddle(d1,d3,om[1]) - * @param h number of "coefficients" h >= 1 - * @param data 4h complex coefficients interleaved and 256b aligned - * @param powom om[0] (re,im) and om[1] where om[1]=om[0]^2 - */ -EXPORT void cplx_bisplit_fft_ref(int32_t h, CPLX* data, const CPLX powom[2]) { - CPLX* d0 = data; - CPLX* d2 = data + 2*h; - const CPLX* om0 = powom; - CPLX iom0; - iom0[0]=powom[0][1]; - iom0[1]=-powom[0][0]; - const CPLX* om1 = powom+1; - cplx_split_fft_ref(h, d0, *om0); - cplx_split_fft_ref(h, d2, iom0); - cplx_split_fft_ref(2*h, d0, *om1); -} - -/** - * Input: Q(y),Q(-y) - * Output: P_0(z),P_1(z) - * where Q(X)=P_0(X^2)+X.P_1(X^2) and y^2 = z - * @param data 2 complexes coefficients interleaved and 256b aligned - * @param powom (z,-z) interleaved: (zre,zim,-zre,-zim) - */ -void split_fft_last_ref(CPLX* data, const CPLX powom) { - CPLX diff; - cplx_sub(diff, data[0], data[1]); - cplx_add(data[0], data[0], data[1]); - cplx_mul(data[1], diff, powom); -}
D spqlios/lib/spqlios/cplx/cplx_conversions.c

@@ -1,158 +0,0 @@

-#include <errno.h> -#include <string.h> - -#include "../commons_private.h" -#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -EXPORT void cplx_from_znx32_ref(const CPLX_FROM_ZNX32_PRECOMP* precomp, void* r, const int32_t* x) { - const uint32_t m = precomp->m; - const int32_t* inre = x; - const int32_t* inim = x + m; - CPLX* out = r; - for (uint32_t i = 0; i < m; ++i) { - out[i][0] = (double)inre[i]; - out[i][1] = (double)inim[i]; - } -} - -EXPORT void cplx_from_tnx32_ref(const CPLX_FROM_TNX32_PRECOMP* precomp, void* r, const int32_t* x) { - static const double _2p32 = 1. / (INT64_C(1) << 32); - const uint32_t m = precomp->m; - const int32_t* inre = x; - const int32_t* inim = x + m; - CPLX* out = r; - for (uint32_t i = 0; i < m; ++i) { - out[i][0] = ((double)inre[i]) * _2p32; - out[i][1] = ((double)inim[i]) * _2p32; - } -} - -EXPORT void cplx_to_tnx32_ref(const CPLX_TO_TNX32_PRECOMP* precomp, int32_t* r, const void* x) { - static const double _2p32 = (INT64_C(1) << 32); - const uint32_t m = precomp->m; - double factor = _2p32 / precomp->divisor; - int32_t* outre = r; - int32_t* outim = r + m; - const CPLX* in = x; - // Note: this formula will only work if abs(in) < 2^32 - for (uint32_t i = 0; i < m; ++i) { - outre[i] = (int32_t)(int64_t)(rint(in[i][0] * factor)); - outim[i] = (int32_t)(int64_t)(rint(in[i][1] * factor)); - } -} - -void* init_cplx_from_znx32_precomp(CPLX_FROM_ZNX32_PRECOMP* res, uint32_t m) { - res->m = m; - if (CPU_SUPPORTS("avx2")) { - if (m >= 8) { - res->function = cplx_from_znx32_avx2_fma; - } else { - res->function = cplx_from_znx32_ref; - } - } else { - res->function = cplx_from_znx32_ref; - } - return res; -} - -CPLX_FROM_ZNX32_PRECOMP* new_cplx_from_znx32_precomp(uint32_t m) { - CPLX_FROM_ZNX32_PRECOMP* res = malloc(sizeof(CPLX_FROM_ZNX32_PRECOMP)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_cplx_from_znx32_precomp(res, m)); -} - -void* init_cplx_from_tnx32_precomp(CPLX_FROM_TNX32_PRECOMP* res, uint32_t m) { - res->m = m; - if (CPU_SUPPORTS("avx2")) { - if (m >= 8) { - res->function = cplx_from_tnx32_avx2_fma; - } else { - res->function = cplx_from_tnx32_ref; - } - } else { - res->function = cplx_from_tnx32_ref; - } - return res; -} - -CPLX_FROM_TNX32_PRECOMP* new_cplx_from_tnx32_precomp(uint32_t m) { - CPLX_FROM_TNX32_PRECOMP* res = malloc(sizeof(CPLX_FROM_TNX32_PRECOMP)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_cplx_from_tnx32_precomp(res, m)); -} - -void* init_cplx_to_tnx32_precomp(CPLX_TO_TNX32_PRECOMP* res, uint32_t m, double divisor, uint32_t log2overhead) { - if (is_not_pow2_double(&divisor)) return spqlios_error("divisor must be a power of 2"); - if (m & (m - 1)) return spqlios_error("m must be a power of 2"); - if (log2overhead > 52) return spqlios_error("log2overhead is too large"); - res->m = m; - res->divisor = divisor; - if (CPU_SUPPORTS("avx2")) { - if (log2overhead <= 18) { - if (m >= 8) { - res->function = cplx_to_tnx32_avx2_fma; - } else { - res->function = cplx_to_tnx32_ref; - } - } else { - res->function = cplx_to_tnx32_ref; - } - } else { - res->function = cplx_to_tnx32_ref; - } - return res; -} - -EXPORT CPLX_TO_TNX32_PRECOMP* new_cplx_to_tnx32_precomp(uint32_t m, double divisor, uint32_t log2overhead) { - CPLX_TO_TNX32_PRECOMP* res = malloc(sizeof(CPLX_TO_TNX32_PRECOMP)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_cplx_to_tnx32_precomp(res, m, divisor, log2overhead)); -} - -/** - * @brief Simpler API for the znx32 to cplx conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void cplx_from_znx32_simple(uint32_t m, void* r, const int32_t* x) { - // not checking for log2bound which is not relevant here - static CPLX_FROM_ZNX32_PRECOMP precomp[32]; - CPLX_FROM_ZNX32_PRECOMP* p = precomp + log2m(m); - if (!p->function) { - if (!init_cplx_from_znx32_precomp(p, m)) abort(); - } - p->function(p, r, x); -} - -/** - * @brief Simpler API for the tnx32 to cplx conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void cplx_from_tnx32_simple(uint32_t m, void* r, const int32_t* x) { - static CPLX_FROM_TNX32_PRECOMP precomp[32]; - CPLX_FROM_TNX32_PRECOMP* p = precomp + log2m(m); - if (!p->function) { - if (!init_cplx_from_tnx32_precomp(p, m)) abort(); - } - p->function(p, r, x); -} -/** - * @brief Simpler API for the cplx to tnx32 conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void cplx_to_tnx32_simple(uint32_t m, double divisor, uint32_t log2overhead, int32_t* r, const void* x) { - struct LAST_CPLX_TO_TNX32_PRECOMP { - CPLX_TO_TNX32_PRECOMP p; - double last_divisor; - double last_log2over; - }; - static __thread struct LAST_CPLX_TO_TNX32_PRECOMP precomp[32]; - struct LAST_CPLX_TO_TNX32_PRECOMP* p = precomp + log2m(m); - if (!p->p.function || divisor != p->last_divisor || log2overhead != p->last_log2over) { - memset(p, 0, sizeof(*p)); - if (!init_cplx_to_tnx32_precomp(&p->p, m, divisor, log2overhead)) abort(); - p->last_divisor = divisor; - p->last_log2over = log2overhead; - } - p->p.function(&p->p, r, x); -}
D spqlios/lib/spqlios/cplx/cplx_conversions_avx2_fma.c

@@ -1,104 +0,0 @@

-#include <immintrin.h> -#include <stdio.h> - -#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -typedef int32_t I8MEM[8]; -typedef double D4MEM[4]; - -__always_inline void cplx_from_any_fma(uint64_t m, void* r, const int32_t* x, const __m256i C, const __m256d R) { - const __m256i S = _mm256_set1_epi32(0x80000000); - const I8MEM* inre = (I8MEM*)(x); - const I8MEM* inim = (I8MEM*)(x+m); - D4MEM* out = (D4MEM*) r; - const uint64_t ms8 = m/8; - for (uint32_t i=0; i<ms8; ++i) { - __m256i rea = _mm256_loadu_si256((__m256i*) inre[0]); - __m256i ima = _mm256_loadu_si256((__m256i*) inim[0]); - rea = _mm256_add_epi32(rea, S); - ima = _mm256_add_epi32(ima, S); - __m256i tmpa = _mm256_unpacklo_epi32(rea, ima); - __m256i tmpc = _mm256_unpackhi_epi32(rea, ima); - __m256i cpla = _mm256_permute2x128_si256(tmpa,tmpc,0x20); - __m256i cplc = _mm256_permute2x128_si256(tmpa,tmpc,0x31); - tmpa = _mm256_unpacklo_epi32(cpla, C); - __m256i tmpb = _mm256_unpackhi_epi32(cpla, C); - tmpc = _mm256_unpacklo_epi32(cplc, C); - __m256i tmpd = _mm256_unpackhi_epi32(cplc, C); - cpla = _mm256_permute2x128_si256(tmpa,tmpb,0x20); - __m256i cplb = _mm256_permute2x128_si256(tmpa,tmpb,0x31); - cplc = _mm256_permute2x128_si256(tmpc,tmpd,0x20); - __m256i cpld = _mm256_permute2x128_si256(tmpc,tmpd,0x31); - __m256d dcpla = _mm256_sub_pd(_mm256_castsi256_pd(cpla), R); - __m256d dcplb = _mm256_sub_pd(_mm256_castsi256_pd(cplb), R); - __m256d dcplc = _mm256_sub_pd(_mm256_castsi256_pd(cplc), R); - __m256d dcpld = _mm256_sub_pd(_mm256_castsi256_pd(cpld), R); - _mm256_storeu_pd(out[0], dcpla); - _mm256_storeu_pd(out[1], dcplb); - _mm256_storeu_pd(out[2], dcplc); - _mm256_storeu_pd(out[3], dcpld); - inre += 1; - inim += 1; - out += 4; - } -} - -EXPORT void cplx_from_znx32_avx2_fma(const CPLX_FROM_ZNX32_PRECOMP* precomp, void* r, const int32_t* x) { - //note: the hex code of 2^31 + 2^52 is 0x4330000080000000 - const __m256i C = _mm256_set1_epi32(0x43300000); - const __m256d R = _mm256_set1_pd((INT64_C(1) << 31) + (INT64_C(1) << 52)); - // double XX = INT64_C(1) + (INT64_C(1)<<31) + (INT64_C(1)<<52); - //printf("\n\n%016lx\n", *(uint64_t*)&XX); - //abort(); - const uint64_t m = precomp->m; - cplx_from_any_fma(m, r, x, C, R); -} - -EXPORT void cplx_from_tnx32_avx2_fma(const CPLX_FROM_TNX32_PRECOMP* precomp, void* r, const int32_t* x) { - //note: the hex code of 2^-1 + 2^30 is 0x4130000080000000 - const __m256i C = _mm256_set1_epi32(0x41300000); - const __m256d R = _mm256_set1_pd(0.5 + (INT64_C(1) << 20)); - // double XX = (double)(INT64_C(1) + (INT64_C(1)<<31) + (INT64_C(1)<<52))/(INT64_C(1)<<32); - //printf("\n\n%016lx\n", *(uint64_t*)&XX); - //abort(); - const uint64_t m = precomp->m; - cplx_from_any_fma(m, r, x, C, R); -} - -EXPORT void cplx_to_tnx32_avx2_fma(const CPLX_TO_TNX32_PRECOMP* precomp, int32_t* r, const void* x) { - const __m256d R = _mm256_set1_pd((0.5 + (INT64_C(3) << 19)) * precomp->divisor); - const __m256i MASK = _mm256_set1_epi64x(0xFFFFFFFFUL); - const __m256i S = _mm256_set1_epi32(0x80000000); - //const __m256i IDX = _mm256_set_epi32(0,4,1,5,2,6,3,7); - const __m256i IDX = _mm256_set_epi32(7,3,6,2,5,1,4,0); - const uint64_t m = precomp->m; - const uint64_t ms8 = m/8; - I8MEM* outre = (I8MEM*) r; - I8MEM* outim = (I8MEM*) (r+m); - const D4MEM* in = x; - // Note: this formula will only work if abs(in) < 2^32 - for (uint32_t i=0; i<ms8; ++i) { - __m256d cpla = _mm256_loadu_pd(in[0]); - __m256d cplb = _mm256_loadu_pd(in[1]); - __m256d cplc = _mm256_loadu_pd(in[2]); - __m256d cpld = _mm256_loadu_pd(in[3]); - __m256i icpla = _mm256_castpd_si256(_mm256_add_pd(cpla, R)); - __m256i icplb = _mm256_castpd_si256(_mm256_add_pd(cplb, R)); - __m256i icplc = _mm256_castpd_si256(_mm256_add_pd(cplc, R)); - __m256i icpld = _mm256_castpd_si256(_mm256_add_pd(cpld, R)); - icpla = _mm256_or_si256(_mm256_and_si256(icpla, MASK), _mm256_slli_epi64(icplb, 32)); - icplc = _mm256_or_si256(_mm256_and_si256(icplc, MASK), _mm256_slli_epi64(icpld, 32)); - icpla = _mm256_xor_si256(icpla, S); - icplc = _mm256_xor_si256(icplc, S); - __m256i re = _mm256_unpacklo_epi64(icpla, icplc); - __m256i im = _mm256_unpackhi_epi64(icpla, icplc); - re = _mm256_permutevar8x32_epi32(re, IDX); - im = _mm256_permutevar8x32_epi32(im, IDX); - _mm256_storeu_si256((__m256i*)outre[0], re); - _mm256_storeu_si256((__m256i*)outim[0], im); - outre += 1; - outim += 1; - in += 4; - } -}
D spqlios/lib/spqlios/cplx/cplx_execute.c

@@ -1,18 +0,0 @@

-#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -EXPORT void cplx_from_znx32(const CPLX_FROM_ZNX32_PRECOMP* tables, void* r, const int32_t* a) { - tables->function(tables, r, a); -} -EXPORT void cplx_from_tnx32(const CPLX_FROM_TNX32_PRECOMP* tables, void* r, const int32_t* a) { - tables->function(tables, r, a); -} -EXPORT void cplx_to_tnx32(const CPLX_TO_TNX32_PRECOMP* tables, int32_t* r, const void* a) { - tables->function(tables, r, a); -} -EXPORT void cplx_fftvec_mul(const CPLX_FFTVEC_MUL_PRECOMP* tables, void* r, const void* a, const void* b) { - tables->function(tables, r, a, b); -} -EXPORT void cplx_fftvec_addmul(const CPLX_FFTVEC_ADDMUL_PRECOMP* tables, void* r, const void* a, const void* b) { - tables->function(tables, r, a, b); -}
D spqlios/lib/spqlios/cplx/cplx_fallbacks_aarch64.c

@@ -1,41 +0,0 @@

-#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -EXPORT void cplx_fftvec_addmul_fma(const CPLX_FFTVEC_ADDMUL_PRECOMP* tables, void* r, const void* a, const void* b) { - UNDEFINED(); // not defined for non x86 targets -} -EXPORT void cplx_fftvec_mul_fma(const CPLX_FFTVEC_MUL_PRECOMP* tables, void* r, const void* a, const void* b) { - UNDEFINED(); -} -EXPORT void cplx_fftvec_addmul_sse(const CPLX_FFTVEC_ADDMUL_PRECOMP* precomp, void* r, const void* a, const void* b) { - UNDEFINED(); -} -EXPORT void cplx_fftvec_addmul_avx512(const CPLX_FFTVEC_ADDMUL_PRECOMP* precomp, void* r, const void* a, - const void* b) { - UNDEFINED(); -} -EXPORT void cplx_fft16_avx_fma(void* data, const void* omega) { UNDEFINED(); } -EXPORT void cplx_ifft16_avx_fma(void* data, const void* omega) { UNDEFINED(); } -EXPORT void cplx_from_znx32_avx2_fma(const CPLX_FROM_ZNX32_PRECOMP* precomp, void* r, const int32_t* x) { UNDEFINED(); } -EXPORT void cplx_from_tnx32_avx2_fma(const CPLX_FROM_TNX32_PRECOMP* precomp, void* r, const int32_t* x) { UNDEFINED(); } -EXPORT void cplx_to_tnx32_avx2_fma(const CPLX_TO_TNX32_PRECOMP* precomp, int32_t* x, const void* c) { UNDEFINED(); } -EXPORT void cplx_fft_avx2_fma(const CPLX_FFT_PRECOMP* tables, void* data){UNDEFINED()} EXPORT - void cplx_ifft_avx2_fma(const CPLX_IFFT_PRECOMP* itables, void* data){UNDEFINED()} EXPORT - void cplx_fftvec_twiddle_fma(const CPLX_FFTVEC_TWIDDLE_PRECOMP* tables, void* a, void* b, const void* om){ - UNDEFINED()} EXPORT void cplx_fftvec_twiddle_avx512(const CPLX_FFTVEC_TWIDDLE_PRECOMP* tables, void* a, void* b, - const void* om){UNDEFINED()} EXPORT - void cplx_fftvec_bitwiddle_fma(const CPLX_FFTVEC_BITWIDDLE_PRECOMP* tables, void* a, uint64_t slice, - const void* om){UNDEFINED()} EXPORT - void cplx_fftvec_bitwiddle_avx512(const CPLX_FFTVEC_BITWIDDLE_PRECOMP* tables, void* a, uint64_t slice, - const void* om){UNDEFINED()} - -// DEPRECATED? -EXPORT void cplx_fftvec_add_fma(uint32_t m, void* r, const void* a, const void* b){UNDEFINED()} EXPORT - void cplx_fftvec_sub2_to_fma(uint32_t m, void* r, const void* a, const void* b){UNDEFINED()} EXPORT - void cplx_fftvec_copy_fma(uint32_t m, void* r, const void* a){UNDEFINED()} - -// executors -//EXPORT void cplx_ifft(const CPLX_IFFT_PRECOMP* itables, void* data) { -// itables->function(itables, data); -//} -//EXPORT void cplx_fft(const CPLX_FFT_PRECOMP* tables, void* data) { tables->function(tables, data); }
D spqlios/lib/spqlios/cplx/cplx_fft.h

@@ -1,221 +0,0 @@

-#ifndef SPQLIOS_CPLX_FFT_H -#define SPQLIOS_CPLX_FFT_H - -#include "../commons.h" - -typedef struct cplx_fft_precomp CPLX_FFT_PRECOMP; -typedef struct cplx_ifft_precomp CPLX_IFFT_PRECOMP; -typedef struct cplx_mul_precomp CPLX_FFTVEC_MUL_PRECOMP; -typedef struct cplx_addmul_precomp CPLX_FFTVEC_ADDMUL_PRECOMP; -typedef struct cplx_from_znx32_precomp CPLX_FROM_ZNX32_PRECOMP; -typedef struct cplx_from_tnx32_precomp CPLX_FROM_TNX32_PRECOMP; -typedef struct cplx_to_tnx32_precomp CPLX_TO_TNX32_PRECOMP; -typedef struct cplx_to_znx32_precomp CPLX_TO_ZNX32_PRECOMP; -typedef struct cplx_from_rnx64_precomp CPLX_FROM_RNX64_PRECOMP; -typedef struct cplx_to_rnx64_precomp CPLX_TO_RNX64_PRECOMP; -typedef struct cplx_round_to_rnx64_precomp CPLX_ROUND_TO_RNX64_PRECOMP; - -/** - * @brief precomputes fft tables. - * The FFT tables contains a constant section that is required for efficient FFT operations in dimension nn. - * The resulting pointer is to be passed as "tables" argument to any call to the fft function. - * The user can optionnally allocate zero or more computation buffers, which are scratch spaces that are contiguous to - * the constant tables in memory, and allow for more efficient operations. It is the user's responsibility to ensure - * that each of those buffers are never used simultaneously by two ffts on different threads at the same time. The fft - * table must be deleted by delete_fft_precomp after its last usage. - */ -EXPORT CPLX_FFT_PRECOMP* new_cplx_fft_precomp(uint32_t m, uint32_t num_buffers); - -/** - * @brief gets the address of a fft buffer allocated during new_fft_precomp. - * This buffer can be used as data pointer in subsequent calls to fft, - * and does not need to be released afterwards. - */ -EXPORT void* cplx_fft_precomp_get_buffer(const CPLX_FFT_PRECOMP* tables, uint32_t buffer_index); - -/** - * @brief allocates a new fft buffer. - * This buffer can be used as data pointer in subsequent calls to fft, - * and must be deleted afterwards by calling delete_fft_buffer. - */ -EXPORT void* new_cplx_fft_buffer(uint32_t m); - -/** - * @brief allocates a new fft buffer. - * This buffer can be used as data pointer in subsequent calls to fft, - * and must be deleted afterwards by calling delete_fft_buffer. - */ -EXPORT void delete_cplx_fft_buffer(void* buffer); - -/** - * @brief deallocates a fft table and all its built-in buffers. - */ -#define delete_cplx_fft_precomp free - -/** - * @brief computes a direct fft in-place over data. - */ -EXPORT void cplx_fft(const CPLX_FFT_PRECOMP* tables, void* data); - -EXPORT CPLX_IFFT_PRECOMP* new_cplx_ifft_precomp(uint32_t m, uint32_t num_buffers); -EXPORT void* cplx_ifft_precomp_get_buffer(const CPLX_IFFT_PRECOMP* tables, uint32_t buffer_index); -EXPORT void cplx_ifft(const CPLX_IFFT_PRECOMP* tables, void* data); -#define delete_cplx_ifft_precomp free - -EXPORT CPLX_FFTVEC_MUL_PRECOMP* new_cplx_fftvec_mul_precomp(uint32_t m); -EXPORT void cplx_fftvec_mul(const CPLX_FFTVEC_MUL_PRECOMP* tables, void* r, const void* a, const void* b); -#define delete_cplx_fftvec_mul_precomp free - -EXPORT CPLX_FFTVEC_ADDMUL_PRECOMP* new_cplx_fftvec_addmul_precomp(uint32_t m); -EXPORT void cplx_fftvec_addmul(const CPLX_FFTVEC_ADDMUL_PRECOMP* tables, void* r, const void* a, const void* b); -#define delete_cplx_fftvec_addmul_precomp free - -/** - * @brief prepares a conversion from ZnX to the cplx layout. - * All the coefficients must be strictly lower than 2^log2bound in absolute value. Any attempt to use - * this function on a larger coefficient is undefined behaviour. The resulting precomputed data must - * be freed with `new_cplx_from_znx32_precomp` - * @param m the target complex dimension m from C[X] mod X^m-i. Note that the inputs have n=2m - * int32 coefficients in natural order modulo X^n+1 - * @param log2bound bound on the input coefficients. Must be between 0 and 32 - */ -EXPORT CPLX_FROM_ZNX32_PRECOMP* new_cplx_from_znx32_precomp(uint32_t m); -/** - * @brief converts from ZnX to the cplx layout. - * @param tables precomputed data obtained by new_cplx_from_znx32_precomp. - * @param r resulting array of m complexes coefficients mod X^m-i - * @param x input array of n bounded integer coefficients mod X^n+1 - */ -EXPORT void cplx_from_znx32(const CPLX_FROM_ZNX32_PRECOMP* tables, void* r, const int32_t* a); -/** @brief frees a precomputed conversion data initialized with new_cplx_from_znx32_precomp. */ -#define delete_cplx_from_znx32_precomp free - -/** - * @brief prepares a conversion from TnX to the cplx layout. - * @param m the target complex dimension m from C[X] mod X^m-i. Note that the inputs have n=2m - * torus32 coefficients. The resulting precomputed data must - * be freed with `delete_cplx_from_tnx32_precomp` - */ -EXPORT CPLX_FROM_TNX32_PRECOMP* new_cplx_from_tnx32_precomp(uint32_t m); -/** - * @brief converts from TnX to the cplx layout. - * @param tables precomputed data obtained by new_cplx_from_tnx32_precomp. - * @param r resulting array of m complexes coefficients mod X^m-i - * @param x input array of n torus32 coefficients mod X^n+1 - */ -EXPORT void cplx_from_tnx32(const CPLX_FROM_TNX32_PRECOMP* tables, void* r, const int32_t* a); -/** @brief frees a precomputed conversion data initialized with new_cplx_from_tnx32_precomp. */ -#define delete_cplx_from_tnx32_precomp free - -/** - * @brief prepares a rescale and conversion from the cplx layout to TnX. - * @param m the target complex dimension m from C[X] mod X^m-i. Note that the outputs have n=2m - * torus32 coefficients. - * @param divisor must be a power of two. The inputs are rescaled by divisor before being reduced modulo 1. - * Remember that the output of an iFFT must be divided by m. - * @param log2overhead all inputs absolute values must be within divisor.2^log2overhead. - * For any inputs outside of these bounds, the conversion is undefined behaviour. - * The maximum supported log2overhead is 52, and the algorithm is faster for log2overhead=18. - */ -EXPORT CPLX_TO_TNX32_PRECOMP* new_cplx_to_tnx32_precomp(uint32_t m, double divisor, uint32_t log2overhead); -/** - * @brief rescale, converts and reduce mod 1 from cplx layout to torus32. - * @param tables precomputed data obtained by new_cplx_from_tnx32_precomp. - * @param r resulting array of n torus32 coefficients mod X^n+1 - * @param x input array of m cplx coefficients mod X^m-i - */ -EXPORT void cplx_to_tnx32(const CPLX_TO_TNX32_PRECOMP* tables, int32_t* r, const void* a); -#define delete_cplx_to_tnx32_precomp free - -EXPORT CPLX_TO_ZNX32_PRECOMP* new_cplx_to_znx32_precomp(uint32_t m, double divisor); -EXPORT void cplx_to_znx32(const CPLX_TO_ZNX32_PRECOMP* precomp, int32_t* r, const void* x); -#define delete_cplx_to_znx32_simple free - -EXPORT CPLX_FROM_RNX64_PRECOMP* new_cplx_from_rnx64_simple(uint32_t m); -EXPORT void cplx_from_rnx64(const CPLX_FROM_RNX64_PRECOMP* precomp, void* r, const double* x); -#define delete_cplx_from_rnx64_simple free - -EXPORT CPLX_TO_RNX64_PRECOMP* new_cplx_to_rnx64(uint32_t m, double divisor); -EXPORT void cplx_to_rnx64(const CPLX_TO_RNX64_PRECOMP* precomp, double* r, const void* x); -#define delete_cplx_round_to_rnx64_simple free - -EXPORT CPLX_ROUND_TO_RNX64_PRECOMP* new_cplx_round_to_rnx64(uint32_t m, double divisor, uint32_t log2bound); -EXPORT void cplx_round_to_rnx64(const CPLX_ROUND_TO_RNX64_PRECOMP* precomp, double* r, const void* x); -#define delete_cplx_round_to_rnx64_simple free - -/** - * @brief Simpler API for the fft function. - * For each dimension, the precomputed tables for this dimension are generated automatically. - * It is advised to do one dry-run per desired dimension before using in a multithread environment */ -EXPORT void cplx_fft_simple(uint32_t m, void* data); -/** - * @brief Simpler API for the ifft function. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension in the main thread before using in a multithread - * environment */ -EXPORT void cplx_ifft_simple(uint32_t m, void* data); -/** - * @brief Simpler API for the fftvec multiplication function. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void cplx_fftvec_mul_simple(uint32_t m, void* r, const void* a, const void* b); -/** - * @brief Simpler API for the fftvec addmul function. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void cplx_fftvec_addmul_simple(uint32_t m, void* r, const void* a, const void* b); -/** - * @brief Simpler API for the znx32 to cplx conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void cplx_from_znx32_simple(uint32_t m, void* r, const int32_t* x); -/** - * @brief Simpler API for the tnx32 to cplx conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void cplx_from_tnx32_simple(uint32_t m, void* r, const int32_t* x); -/** - * @brief Simpler API for the cplx to tnx32 conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void cplx_to_tnx32_simple(uint32_t m, double divisor, uint32_t log2overhead, int32_t* r, const void* x); - -/** - * @brief converts, divides and round from cplx to znx32 (simple API) - * @param m the complex dimension - * @param divisor the divisor: a power of two, often m after an ifft - * @param r the result: must be a double array of size 2m. r must be distinct from x - * @param x the input: must hold m complex numbers. - */ -EXPORT void cplx_to_znx32_simple(uint32_t m, double divisor, int32_t* r, const void* x); - -/** - * @brief converts from rnx64 to cplx (simple API) - * The bound on the output is assumed to be within ]2^-31,2^31[. - * Any coefficient that would fall outside this range is undefined behaviour. - * @param m the complex dimension - * @param r the result: must be an array of m complex numbers. r must be distinct from x - * @param x the input: must be an array of 2m doubles. - */ -EXPORT void cplx_from_rnx64_simple(uint32_t m, void* r, const double* x); - -/** - * @brief converts, divides from cplx to rnx64 (simple API) - * @param m the complex dimension - * @param divisor the divisor: a power of two, often m after an ifft - * @param r the result: must be a double array of size 2m. r must be distinct from x - * @param x the input: must hold m complex numbers. - */ -EXPORT void cplx_to_rnx64_simple(uint32_t m, double divisor, double* r, const void* x); - -/** - * @brief converts, divides and round to integer from cplx to rnx32 (simple API) - * @param m the complex dimension - * @param divisor the divisor: a power of two, often m after an ifft - * @param log2bound a guarantee on the log2bound of the output. log2bound<=48 will use a more efficient algorithm. - * @param r the result: must be a double array of size 2m. r must be distinct from x - * @param x the input: must hold m complex numbers. - */ -EXPORT void cplx_round_to_rnx64_simple(uint32_t m, double divisor, uint32_t log2bound, double* r, const void* x); - -#endif // SPQLIOS_CPLX_FFT_H
D spqlios/lib/spqlios/cplx/cplx_fft16_avx_fma.s

@@ -1,156 +0,0 @@

-# shifted FFT over X^16-i -# 1st argument (rdi) contains 16 complexes -# 2nd argument (rsi) contains: 8 complexes -# omega,alpha,beta,j.beta,gamma,j.gamma,k.gamma,kj.gamma -# alpha = sqrt(omega), beta = sqrt(alpha), gamma = sqrt(beta) -# j = sqrt(i), k=sqrt(j) -.globl cplx_fft16_avx_fma -cplx_fft16_avx_fma: -vmovupd (%rdi),%ymm8 -vmovupd 0x20(%rdi),%ymm9 -vmovupd 0x40(%rdi),%ymm10 -vmovupd 0x60(%rdi),%ymm11 -vmovupd 0x80(%rdi),%ymm12 -vmovupd 0xa0(%rdi),%ymm13 -vmovupd 0xc0(%rdi),%ymm14 -vmovupd 0xe0(%rdi),%ymm15 - -.first_pass: -vmovupd (%rsi),%xmm0 /* omri */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 /* omriri */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: omiiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: omrrrr */ -vshufpd $5, %ymm12, %ymm12, %ymm4 -vshufpd $5, %ymm13, %ymm13, %ymm5 -vshufpd $5, %ymm14, %ymm14, %ymm6 -vshufpd $5, %ymm15, %ymm15, %ymm7 -vmulpd %ymm4,%ymm1,%ymm4 -vmulpd %ymm5,%ymm1,%ymm5 -vmulpd %ymm6,%ymm1,%ymm6 -vmulpd %ymm7,%ymm1,%ymm7 -vfmaddsub231pd %ymm12, %ymm0, %ymm4 # ymm4 = (ymm0 * ymm12) +/- ymm4 -vfmaddsub231pd %ymm13, %ymm0, %ymm5 -vfmaddsub231pd %ymm14, %ymm0, %ymm6 -vfmaddsub231pd %ymm15, %ymm0, %ymm7 -vsubpd %ymm4,%ymm8,%ymm12 -vsubpd %ymm5,%ymm9,%ymm13 -vsubpd %ymm6,%ymm10,%ymm14 -vsubpd %ymm7,%ymm11,%ymm15 -vaddpd %ymm4,%ymm8,%ymm8 -vaddpd %ymm5,%ymm9,%ymm9 -vaddpd %ymm6,%ymm10,%ymm10 -vaddpd %ymm7,%ymm11,%ymm11 - -.second_pass: -vmovupd 16(%rsi),%xmm0 /* omri */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 /* omriri */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: omiiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: omrrrr */ -vshufpd $5, %ymm10, %ymm10, %ymm4 -vshufpd $5, %ymm11, %ymm11, %ymm5 -vshufpd $5, %ymm14, %ymm14, %ymm6 -vshufpd $5, %ymm15, %ymm15, %ymm7 -vmulpd %ymm4,%ymm1,%ymm4 -vmulpd %ymm5,%ymm1,%ymm5 -vmulpd %ymm6,%ymm0,%ymm6 -vmulpd %ymm7,%ymm0,%ymm7 -vfmaddsub231pd %ymm10, %ymm0, %ymm4 # ymm4 = (ymm0 * ymm10) +/- ymm4 -vfmaddsub231pd %ymm11, %ymm0, %ymm5 -vfmsubadd231pd %ymm14, %ymm1, %ymm6 -vfmsubadd231pd %ymm15, %ymm1, %ymm7 -vsubpd %ymm4,%ymm8,%ymm10 -vsubpd %ymm5,%ymm9,%ymm11 -vaddpd %ymm6,%ymm12,%ymm14 -vaddpd %ymm7,%ymm13,%ymm15 -vaddpd %ymm4,%ymm8,%ymm8 -vaddpd %ymm5,%ymm9,%ymm9 -vsubpd %ymm6,%ymm12,%ymm12 -vsubpd %ymm7,%ymm13,%ymm13 - -.third_pass: -vmovupd 32(%rsi),%xmm0 /* gamma */ -vmovupd 48(%rsi),%xmm2 /* delta */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 -vinsertf128 $1, %xmm2, %ymm2, %ymm2 -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: gama.iiii */ -vshufpd $15, %ymm2, %ymm2, %ymm3 /* ymm3: delta.iiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: gama.rrrr */ -vshufpd $0, %ymm2, %ymm2, %ymm2 /* ymm2: delta.rrrr */ -vshufpd $5, %ymm9, %ymm9, %ymm4 -vshufpd $5, %ymm11, %ymm11, %ymm5 -vshufpd $5, %ymm13, %ymm13, %ymm6 -vshufpd $5, %ymm15, %ymm15, %ymm7 -vmulpd %ymm4,%ymm1,%ymm4 -vmulpd %ymm5,%ymm0,%ymm5 -vmulpd %ymm6,%ymm3,%ymm6 -vmulpd %ymm7,%ymm2,%ymm7 -vfmaddsub231pd %ymm9, %ymm0, %ymm4 # ymm4 = (ymm0 * ymm10) +/- ymm4 -vfmsubadd231pd %ymm11, %ymm1, %ymm5 -vfmaddsub231pd %ymm13, %ymm2, %ymm6 -vfmsubadd231pd %ymm15, %ymm3, %ymm7 -vsubpd %ymm4,%ymm8,%ymm9 -vaddpd %ymm5,%ymm10,%ymm11 -vsubpd %ymm6,%ymm12,%ymm13 -vaddpd %ymm7,%ymm14,%ymm15 -vaddpd %ymm4,%ymm8,%ymm8 -vsubpd %ymm5,%ymm10,%ymm10 -vaddpd %ymm6,%ymm12,%ymm12 -vsubpd %ymm7,%ymm14,%ymm14 - -.fourth_pass: -vmovupd 64(%rsi),%ymm0 /* gamma */ -vmovupd 96(%rsi),%ymm2 /* delta */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: gama.iiii */ -vshufpd $15, %ymm2, %ymm2, %ymm3 /* ymm3: delta.iiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: gama.rrrr */ -vshufpd $0, %ymm2, %ymm2, %ymm2 /* ymm2: delta.rrrr */ -vperm2f128 $0x31,%ymm10,%ymm8,%ymm4 # ymm4 contains c1,c5 -- x gamma -vperm2f128 $0x31,%ymm11,%ymm9,%ymm5 # ymm5 contains c3,c7 -- x igamma -vperm2f128 $0x31,%ymm14,%ymm12,%ymm6 # ymm6 contains c9,c13 -- x delta -vperm2f128 $0x31,%ymm15,%ymm13,%ymm7 # ymm7 contains c11,c15 -- x idelta -vperm2f128 $0x20,%ymm10,%ymm8,%ymm8 # ymm8 contains c0,c4 -vperm2f128 $0x20,%ymm11,%ymm9,%ymm9 # ymm9 contains c2,c6 -vperm2f128 $0x20,%ymm14,%ymm12,%ymm10 # ymm10 contains c8,c12 -vperm2f128 $0x20,%ymm15,%ymm13,%ymm11 # ymm11 contains c10,c14 -vshufpd $5, %ymm4, %ymm4, %ymm12 -vshufpd $5, %ymm5, %ymm5, %ymm13 -vshufpd $5, %ymm6, %ymm6, %ymm14 -vshufpd $5, %ymm7, %ymm7, %ymm15 -vmulpd %ymm12,%ymm1,%ymm12 -vmulpd %ymm13,%ymm0,%ymm13 -vmulpd %ymm14,%ymm3,%ymm14 -vmulpd %ymm15,%ymm2,%ymm15 -vfmaddsub231pd %ymm4, %ymm0, %ymm12 # ymm12 = (ymm0 * ymm4) +/- ymm12 -vfmsubadd231pd %ymm5, %ymm1, %ymm13 -vfmaddsub231pd %ymm6, %ymm2, %ymm14 -vfmsubadd231pd %ymm7, %ymm3, %ymm15 -vsubpd %ymm12,%ymm8,%ymm4 -vaddpd %ymm13,%ymm9,%ymm5 -vsubpd %ymm14,%ymm10,%ymm6 -vaddpd %ymm15,%ymm11,%ymm7 -vaddpd %ymm12,%ymm8,%ymm8 -vsubpd %ymm13,%ymm9,%ymm9 -vaddpd %ymm14,%ymm10,%ymm10 -vsubpd %ymm15,%ymm11,%ymm11 - -vperm2f128 $0x20,%ymm6,%ymm10,%ymm12 # ymm4 contains c1,c5 -- x gamma -vperm2f128 $0x20,%ymm7,%ymm11,%ymm13 # ymm5 contains c3,c7 -- x igamma -vperm2f128 $0x31,%ymm6,%ymm10,%ymm14 # ymm6 contains c9,c13 -- x delta -vperm2f128 $0x31,%ymm7,%ymm11,%ymm15 # ymm7 contains c11,c15 -- x idelta -vperm2f128 $0x31,%ymm4,%ymm8,%ymm10 # ymm10 contains c8,c12 -vperm2f128 $0x31,%ymm5,%ymm9,%ymm11 # ymm11 contains c10,c14 -vperm2f128 $0x20,%ymm4,%ymm8,%ymm8 # ymm8 contains c0,c4 -vperm2f128 $0x20,%ymm5,%ymm9,%ymm9 # ymm9 contains c2,c6 - -.save_and_return: -vmovupd %ymm8,(%rdi) -vmovupd %ymm9,0x20(%rdi) -vmovupd %ymm10,0x40(%rdi) -vmovupd %ymm11,0x60(%rdi) -vmovupd %ymm12,0x80(%rdi) -vmovupd %ymm13,0xa0(%rdi) -vmovupd %ymm14,0xc0(%rdi) -vmovupd %ymm15,0xe0(%rdi) -ret -.size cplx_fft16_avx_fma, .-cplx_fft16_avx_fma -.section .note.GNU-stack,"",@progbits
D spqlios/lib/spqlios/cplx/cplx_fft16_avx_fma_win32.s

@@ -1,190 +0,0 @@

- .text - .p2align 4 - .globl cplx_fft16_avx_fma - .def cplx_fft16_avx_fma; .scl 2; .type 32; .endef -cplx_fft16_avx_fma: - - pushq %rdi - pushq %rsi - movq %rcx,%rdi - movq %rdx,%rsi - subq $0x100,%rsp - movdqu %xmm6,(%rsp) - movdqu %xmm7,0x10(%rsp) - movdqu %xmm8,0x20(%rsp) - movdqu %xmm9,0x30(%rsp) - movdqu %xmm10,0x40(%rsp) - movdqu %xmm11,0x50(%rsp) - movdqu %xmm12,0x60(%rsp) - movdqu %xmm13,0x70(%rsp) - movdqu %xmm14,0x80(%rsp) - movdqu %xmm15,0x90(%rsp) - callq cplx_fft16_avx_fma_amd64 - movdqu (%rsp),%xmm6 - movdqu 0x10(%rsp),%xmm7 - movdqu 0x20(%rsp),%xmm8 - movdqu 0x30(%rsp),%xmm9 - movdqu 0x40(%rsp),%xmm10 - movdqu 0x50(%rsp),%xmm11 - movdqu 0x60(%rsp),%xmm12 - movdqu 0x70(%rsp),%xmm13 - movdqu 0x80(%rsp),%xmm14 - movdqu 0x90(%rsp),%xmm15 - addq $0x100,%rsp - popq %rsi - popq %rdi - retq - -# shifted FFT over X^16-i -# 1st argument (rdi) contains 16 complexes -# 2nd argument (rsi) contains: 8 complexes -# omega,alpha,beta,j.beta,gamma,j.gamma,k.gamma,kj.gamma -# alpha = sqrt(omega), beta = sqrt(alpha), gamma = sqrt(beta) -# j = sqrt(i), k=sqrt(j) -cplx_fft16_avx_fma_amd64: -vmovupd (%rdi),%ymm8 -vmovupd 0x20(%rdi),%ymm9 -vmovupd 0x40(%rdi),%ymm10 -vmovupd 0x60(%rdi),%ymm11 -vmovupd 0x80(%rdi),%ymm12 -vmovupd 0xa0(%rdi),%ymm13 -vmovupd 0xc0(%rdi),%ymm14 -vmovupd 0xe0(%rdi),%ymm15 - -.first_pass: -vmovupd (%rsi),%xmm0 /* omri */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 /* omriri */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: omiiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: omrrrr */ -vshufpd $5, %ymm12, %ymm12, %ymm4 -vshufpd $5, %ymm13, %ymm13, %ymm5 -vshufpd $5, %ymm14, %ymm14, %ymm6 -vshufpd $5, %ymm15, %ymm15, %ymm7 -vmulpd %ymm4,%ymm1,%ymm4 -vmulpd %ymm5,%ymm1,%ymm5 -vmulpd %ymm6,%ymm1,%ymm6 -vmulpd %ymm7,%ymm1,%ymm7 -vfmaddsub231pd %ymm12, %ymm0, %ymm4 # ymm4 = (ymm0 * ymm12) +/- ymm4 -vfmaddsub231pd %ymm13, %ymm0, %ymm5 -vfmaddsub231pd %ymm14, %ymm0, %ymm6 -vfmaddsub231pd %ymm15, %ymm0, %ymm7 -vsubpd %ymm4,%ymm8,%ymm12 -vsubpd %ymm5,%ymm9,%ymm13 -vsubpd %ymm6,%ymm10,%ymm14 -vsubpd %ymm7,%ymm11,%ymm15 -vaddpd %ymm4,%ymm8,%ymm8 -vaddpd %ymm5,%ymm9,%ymm9 -vaddpd %ymm6,%ymm10,%ymm10 -vaddpd %ymm7,%ymm11,%ymm11 - -.second_pass: -vmovupd 16(%rsi),%xmm0 /* omri */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 /* omriri */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: omiiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: omrrrr */ -vshufpd $5, %ymm10, %ymm10, %ymm4 -vshufpd $5, %ymm11, %ymm11, %ymm5 -vshufpd $5, %ymm14, %ymm14, %ymm6 -vshufpd $5, %ymm15, %ymm15, %ymm7 -vmulpd %ymm4,%ymm1,%ymm4 -vmulpd %ymm5,%ymm1,%ymm5 -vmulpd %ymm6,%ymm0,%ymm6 -vmulpd %ymm7,%ymm0,%ymm7 -vfmaddsub231pd %ymm10, %ymm0, %ymm4 # ymm4 = (ymm0 * ymm10) +/- ymm4 -vfmaddsub231pd %ymm11, %ymm0, %ymm5 -vfmsubadd231pd %ymm14, %ymm1, %ymm6 -vfmsubadd231pd %ymm15, %ymm1, %ymm7 -vsubpd %ymm4,%ymm8,%ymm10 -vsubpd %ymm5,%ymm9,%ymm11 -vaddpd %ymm6,%ymm12,%ymm14 -vaddpd %ymm7,%ymm13,%ymm15 -vaddpd %ymm4,%ymm8,%ymm8 -vaddpd %ymm5,%ymm9,%ymm9 -vsubpd %ymm6,%ymm12,%ymm12 -vsubpd %ymm7,%ymm13,%ymm13 - -.third_pass: -vmovupd 32(%rsi),%xmm0 /* gamma */ -vmovupd 48(%rsi),%xmm2 /* delta */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 -vinsertf128 $1, %xmm2, %ymm2, %ymm2 -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: gama.iiii */ -vshufpd $15, %ymm2, %ymm2, %ymm3 /* ymm3: delta.iiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: gama.rrrr */ -vshufpd $0, %ymm2, %ymm2, %ymm2 /* ymm2: delta.rrrr */ -vshufpd $5, %ymm9, %ymm9, %ymm4 -vshufpd $5, %ymm11, %ymm11, %ymm5 -vshufpd $5, %ymm13, %ymm13, %ymm6 -vshufpd $5, %ymm15, %ymm15, %ymm7 -vmulpd %ymm4,%ymm1,%ymm4 -vmulpd %ymm5,%ymm0,%ymm5 -vmulpd %ymm6,%ymm3,%ymm6 -vmulpd %ymm7,%ymm2,%ymm7 -vfmaddsub231pd %ymm9, %ymm0, %ymm4 # ymm4 = (ymm0 * ymm10) +/- ymm4 -vfmsubadd231pd %ymm11, %ymm1, %ymm5 -vfmaddsub231pd %ymm13, %ymm2, %ymm6 -vfmsubadd231pd %ymm15, %ymm3, %ymm7 -vsubpd %ymm4,%ymm8,%ymm9 -vaddpd %ymm5,%ymm10,%ymm11 -vsubpd %ymm6,%ymm12,%ymm13 -vaddpd %ymm7,%ymm14,%ymm15 -vaddpd %ymm4,%ymm8,%ymm8 -vsubpd %ymm5,%ymm10,%ymm10 -vaddpd %ymm6,%ymm12,%ymm12 -vsubpd %ymm7,%ymm14,%ymm14 - -.fourth_pass: -vmovupd 64(%rsi),%ymm0 /* gamma */ -vmovupd 96(%rsi),%ymm2 /* delta */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: gama.iiii */ -vshufpd $15, %ymm2, %ymm2, %ymm3 /* ymm3: delta.iiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: gama.rrrr */ -vshufpd $0, %ymm2, %ymm2, %ymm2 /* ymm2: delta.rrrr */ -vperm2f128 $0x31,%ymm10,%ymm8,%ymm4 # ymm4 contains c1,c5 -- x gamma -vperm2f128 $0x31,%ymm11,%ymm9,%ymm5 # ymm5 contains c3,c7 -- x igamma -vperm2f128 $0x31,%ymm14,%ymm12,%ymm6 # ymm6 contains c9,c13 -- x delta -vperm2f128 $0x31,%ymm15,%ymm13,%ymm7 # ymm7 contains c11,c15 -- x idelta -vperm2f128 $0x20,%ymm10,%ymm8,%ymm8 # ymm8 contains c0,c4 -vperm2f128 $0x20,%ymm11,%ymm9,%ymm9 # ymm9 contains c2,c6 -vperm2f128 $0x20,%ymm14,%ymm12,%ymm10 # ymm10 contains c8,c12 -vperm2f128 $0x20,%ymm15,%ymm13,%ymm11 # ymm11 contains c10,c14 -vshufpd $5, %ymm4, %ymm4, %ymm12 -vshufpd $5, %ymm5, %ymm5, %ymm13 -vshufpd $5, %ymm6, %ymm6, %ymm14 -vshufpd $5, %ymm7, %ymm7, %ymm15 -vmulpd %ymm12,%ymm1,%ymm12 -vmulpd %ymm13,%ymm0,%ymm13 -vmulpd %ymm14,%ymm3,%ymm14 -vmulpd %ymm15,%ymm2,%ymm15 -vfmaddsub231pd %ymm4, %ymm0, %ymm12 # ymm12 = (ymm0 * ymm4) +/- ymm12 -vfmsubadd231pd %ymm5, %ymm1, %ymm13 -vfmaddsub231pd %ymm6, %ymm2, %ymm14 -vfmsubadd231pd %ymm7, %ymm3, %ymm15 -vsubpd %ymm12,%ymm8,%ymm4 -vaddpd %ymm13,%ymm9,%ymm5 -vsubpd %ymm14,%ymm10,%ymm6 -vaddpd %ymm15,%ymm11,%ymm7 -vaddpd %ymm12,%ymm8,%ymm8 -vsubpd %ymm13,%ymm9,%ymm9 -vaddpd %ymm14,%ymm10,%ymm10 -vsubpd %ymm15,%ymm11,%ymm11 - -vperm2f128 $0x20,%ymm6,%ymm10,%ymm12 # ymm4 contains c1,c5 -- x gamma -vperm2f128 $0x20,%ymm7,%ymm11,%ymm13 # ymm5 contains c3,c7 -- x igamma -vperm2f128 $0x31,%ymm6,%ymm10,%ymm14 # ymm6 contains c9,c13 -- x delta -vperm2f128 $0x31,%ymm7,%ymm11,%ymm15 # ymm7 contains c11,c15 -- x idelta -vperm2f128 $0x31,%ymm4,%ymm8,%ymm10 # ymm10 contains c8,c12 -vperm2f128 $0x31,%ymm5,%ymm9,%ymm11 # ymm11 contains c10,c14 -vperm2f128 $0x20,%ymm4,%ymm8,%ymm8 # ymm8 contains c0,c4 -vperm2f128 $0x20,%ymm5,%ymm9,%ymm9 # ymm9 contains c2,c6 - -.save_and_return: -vmovupd %ymm8,(%rdi) -vmovupd %ymm9,0x20(%rdi) -vmovupd %ymm10,0x40(%rdi) -vmovupd %ymm11,0x60(%rdi) -vmovupd %ymm12,0x80(%rdi) -vmovupd %ymm13,0xa0(%rdi) -vmovupd %ymm14,0xc0(%rdi) -vmovupd %ymm15,0xe0(%rdi) -ret
D spqlios/lib/spqlios/cplx/cplx_fft_asserts.c

@@ -1,8 +0,0 @@

-#include "cplx_fft_private.h" -#include "../commons_private.h" - -__always_inline void my_asserts() { - STATIC_ASSERT(sizeof(FFT_FUNCTION)==8); - STATIC_ASSERT(sizeof(CPLX_FFT_PRECOMP)==40); - STATIC_ASSERT(sizeof(CPLX_IFFT_PRECOMP)==40); -}
D spqlios/lib/spqlios/cplx/cplx_fft_avx2_fma.c

@@ -1,266 +0,0 @@

-#include <immintrin.h> -#include <string.h> - -#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -typedef double D4MEM[4]; - -/** - * @brief complex fft via bfs strategy (for m between 2 and 8) - * @param dat the data to run the algorithm on - * @param omg precomputed tables (must have been filled with fill_omega) - * @param m ring dimension of the FFT (modulo X^m-i) - */ -void cplx_fft_avx2_fma_bfs_2(D4MEM* dat, const D4MEM** omg, uint32_t m) { - double* data = (double*)dat; - int32_t _2nblock = m >> 1; // = h in ref code - D4MEM* const finaldd = (D4MEM*)(data + 2 * m); - while (_2nblock >= 2) { - int32_t nblock = _2nblock >> 1; // =h/2 in ref code - D4MEM* dd = (D4MEM*)data; - do { - const __m256d om = _mm256_load_pd(*omg[0]); - const __m256d omim = _mm256_addsub_pd(_mm256_setzero_pd(), _mm256_unpackhi_pd(om, om)); - const __m256d omre = _mm256_unpacklo_pd(om, om); - D4MEM* const ddend = (dd + nblock); - D4MEM* ddmid = ddend; - do { - const __m256d b = _mm256_loadu_pd(ddmid[0]); - const __m256d t1 = _mm256_mul_pd(b, omre); - const __m256d barb = _mm256_shuffle_pd(b, b, 5); - const __m256d t2 = _mm256_fmadd_pd(barb, omim, t1); - const __m256d a = _mm256_loadu_pd(dd[0]); - const __m256d newa = _mm256_add_pd(a, t2); - const __m256d newb = _mm256_sub_pd(a, t2); - _mm256_storeu_pd(dd[0], newa); - _mm256_storeu_pd(ddmid[0], newb); - dd += 1; - ddmid += 1; - } while (dd < ddend); - dd += nblock; - *omg += 1; - } while (dd < finaldd); - _2nblock >>= 1; - } - // last iteration when _2nblock == 1 - { - D4MEM* dd = (D4MEM*)data; - do { - const __m256d om = _mm256_load_pd(*omg[0]); - const __m256d omre = _mm256_unpacklo_pd(om, om); - const __m256d omim = _mm256_unpackhi_pd(om, om); - const __m256d ab = _mm256_loadu_pd(dd[0]); - const __m256d bb = _mm256_permute4x64_pd(ab, 0b11101110); - const __m256d bbbar = _mm256_permute4x64_pd(ab, 0b10111011); - const __m256d t1 = _mm256_mul_pd(bbbar, omim); - const __m256d t2 = _mm256_fmaddsub_pd(bb, omre, t1); - const __m256d aa = _mm256_permute4x64_pd(ab, 0b01000100); - const __m256d newab = _mm256_add_pd(aa, t2); - _mm256_storeu_pd(dd[0], newab); - dd += 1; - *omg += 1; - } while (dd < finaldd); - } -} - -__always_inline void cplx_twiddle_fft_avx2(int32_t h, D4MEM* data, const void* omg) { - const __m256d om = _mm256_loadu_pd(omg); - const __m256d omim = _mm256_unpackhi_pd(om, om); - const __m256d omre = _mm256_unpacklo_pd(om, om); - D4MEM* d0 = data; - D4MEM* const ddend = d0 + (h>>1); - D4MEM* d1 = ddend; - do { - const __m256d b = _mm256_loadu_pd(d1[0]); - const __m256d barb = _mm256_shuffle_pd(b, b, 5); - const __m256d t1 = _mm256_mul_pd(barb, omim); - const __m256d t2 = _mm256_fmaddsub_pd(b, omre, t1); - const __m256d a = _mm256_loadu_pd(d0[0]); - const __m256d newa = _mm256_add_pd(a, t2); - const __m256d newb = _mm256_sub_pd(a, t2); - _mm256_storeu_pd(d0[0], newa); - _mm256_storeu_pd(d1[0], newb); - d0 += 1; - d1 += 1; - } while (d0 < ddend); -} - -__always_inline void cplx_bitwiddle_fft_avx2(int32_t h, void* data, const void* powom) { - const __m256d omx = _mm256_loadu_pd(powom); - const __m256d oma = _mm256_permute2f128_pd(omx, omx, 0x00); - const __m256d omb = _mm256_permute2f128_pd(omx, omx, 0x11); - const __m256d omaim = _mm256_unpackhi_pd(oma, oma); - const __m256d omare = _mm256_unpacklo_pd(oma, oma); - const __m256d ombim = _mm256_unpackhi_pd(omb, omb); - const __m256d ombre = _mm256_unpacklo_pd(omb, omb); - D4MEM* d0 = (D4MEM*) data; - D4MEM* const ddend = d0 + (h>>1); - D4MEM* d1 = ddend; - D4MEM* d2 = d0+h; - D4MEM* d3 = d1+h; - __m256d reg0,reg1,reg2,reg3,tmp0,tmp1; - do { - reg0 = _mm256_loadu_pd(d0[0]); - reg1 = _mm256_loadu_pd(d1[0]); - reg2 = _mm256_loadu_pd(d2[0]); - reg3 = _mm256_loadu_pd(d3[0]); - tmp0 = _mm256_shuffle_pd(reg2, reg2, 5); - tmp1 = _mm256_shuffle_pd(reg3, reg3, 5); - tmp0 = _mm256_mul_pd(tmp0, omaim); - tmp1 = _mm256_mul_pd(tmp1, omaim); - tmp0 = _mm256_fmaddsub_pd(reg2, omare, tmp0); - tmp1 = _mm256_fmaddsub_pd(reg3, omare, tmp1); - reg2 = _mm256_sub_pd(reg0, tmp0); - reg3 = _mm256_sub_pd(reg1, tmp1); - reg0 = _mm256_add_pd(reg0, tmp0); - reg1 = _mm256_add_pd(reg1, tmp1); - //-------------------------------------- - tmp0 = _mm256_shuffle_pd(reg1, reg1, 5); - tmp1 = _mm256_shuffle_pd(reg3, reg3, 5); - tmp0 = _mm256_mul_pd(tmp0, ombim); //(r,i) - tmp1 = _mm256_mul_pd(tmp1, ombre); //(-i,r) - tmp0 = _mm256_fmaddsub_pd(reg1, ombre, tmp0); - tmp1 = _mm256_fmsubadd_pd(reg3, ombim, tmp1); - reg1 = _mm256_sub_pd(reg0, tmp0); - reg3 = _mm256_add_pd(reg2, tmp1); - reg0 = _mm256_add_pd(reg0, tmp0); - reg2 = _mm256_sub_pd(reg2, tmp1); - ///// - _mm256_storeu_pd(d0[0], reg0); - _mm256_storeu_pd(d1[0], reg1); - _mm256_storeu_pd(d2[0], reg2); - _mm256_storeu_pd(d3[0], reg3); - d0 += 1; - d1 += 1; - d2 += 1; - d3 += 1; - } while (d0 < ddend); -} - -/** - * @brief complex fft via bfs strategy (for m >= 16) - * @param dat the data to run the algorithm on - * @param omg precomputed tables (must have been filled with fill_omega) - * @param m ring dimension of the FFT (modulo X^m-i) - */ -void cplx_fft_avx2_fma_bfs_16(D4MEM* dat, const D4MEM** omg, uint32_t m) { - double* data = (double*)dat; - D4MEM* const finaldd = (D4MEM*)(data + 2 * m); - uint32_t mm = m; - uint32_t log2m = _mm_popcnt_u32(m-1); // log2(m) - if (log2m % 2 == 1) { - uint32_t h = mm>>1; - cplx_twiddle_fft_avx2(h, dat, **omg); - *omg += 1; - mm >>= 1; - } - while(mm>16) { - uint32_t h = mm/4; - for (CPLX* d = (CPLX*) data; d < (CPLX*) finaldd; d += mm) { - cplx_bitwiddle_fft_avx2(h, d, (CPLX*) *omg); - *omg += 1; - } - mm=h; - } - { - D4MEM* dd = (D4MEM*)data; - do { - cplx_fft16_avx_fma(dd, *omg); - dd += 8; - *omg += 4; - } while (dd < finaldd); - _mm256_zeroupper(); - } - /* - int32_t _2nblock = m >> 1; // = h in ref code - while (_2nblock >= 16) { - int32_t nblock = _2nblock >> 1; // =h/2 in ref code - D4MEM* dd = (D4MEM*)data; - do { - const __m256d om = _mm256_load_pd(*omg[0]); - const __m256d omim = _mm256_addsub_pd(_mm256_setzero_pd(), _mm256_unpackhi_pd(om, om)); - const __m256d omre = _mm256_unpacklo_pd(om, om); - D4MEM* const ddend = (dd + nblock); - D4MEM* ddmid = ddend; - do { - const __m256d b = _mm256_loadu_pd(ddmid[0]); - const __m256d t1 = _mm256_mul_pd(b, omre); - const __m256d barb = _mm256_shuffle_pd(b, b, 5); - const __m256d t2 = _mm256_fmadd_pd(barb, omim, t1); - const __m256d a = _mm256_loadu_pd(dd[0]); - const __m256d newa = _mm256_add_pd(a, t2); - const __m256d newb = _mm256_sub_pd(a, t2); - _mm256_storeu_pd(dd[0], newa); - _mm256_storeu_pd(ddmid[0], newb); - dd += 1; - ddmid += 1; - } while (dd < ddend); - dd += nblock; - *omg += 1; - } while (dd < finaldd); - _2nblock >>= 1; - } - // last iteration when _2nblock == 8 - { - D4MEM* dd = (D4MEM*)data; - do { - cplx_fft16_avx_fma(dd, *omg); - dd += 8; - *omg += 4; - } while (dd < finaldd); - _mm256_zeroupper(); - } - */ -} - -/** - * @brief complex fft via dfs recursion (for m >= 16) - * @param dat the data to run the algorithm on - * @param omg precomputed tables (must have been filled with fill_omega) - * @param m ring dimension of the FFT (modulo X^m-i) - */ -void cplx_fft_avx2_fma_rec_16(D4MEM* dat, const D4MEM** omg, uint32_t m) { - if (m <= 8) return cplx_fft_avx2_fma_bfs_2(dat, omg, m); - if (m <= 2048) return cplx_fft_avx2_fma_bfs_16(dat, omg, m); - double* data = (double*)dat; - int32_t _2nblock = m >> 1; // = h in ref code - int32_t nblock = _2nblock >> 1; // =h/2 in ref code - D4MEM* dd = (D4MEM*)data; - const __m256d om = _mm256_load_pd(*omg[0]); - const __m256d omim = _mm256_addsub_pd(_mm256_setzero_pd(), _mm256_unpackhi_pd(om, om)); - const __m256d omre = _mm256_unpacklo_pd(om, om); - D4MEM* const ddend = (dd + nblock); - D4MEM* ddmid = ddend; - do { - const __m256d b = _mm256_loadu_pd(ddmid[0]); - const __m256d t1 = _mm256_mul_pd(b, omre); - const __m256d barb = _mm256_shuffle_pd(b, b, 5); - const __m256d t2 = _mm256_fmadd_pd(barb, omim, t1); - const __m256d a = _mm256_loadu_pd(dd[0]); - const __m256d newa = _mm256_add_pd(a, t2); - const __m256d newb = _mm256_sub_pd(a, t2); - _mm256_storeu_pd(dd[0], newa); - _mm256_storeu_pd(ddmid[0], newb); - dd += 1; - ddmid += 1; - } while (dd < ddend); - *omg += 1; - cplx_fft_avx2_fma_rec_16(dat, omg, _2nblock); - cplx_fft_avx2_fma_rec_16(ddend, omg, _2nblock); -} - -/** - * @brief complex fft via best strategy (for m>=1) - * @param dat the data to run the algorithm on: m complex numbers - * @param omg precomputed tables (must have been filled with fill_omega) - * @param m ring dimension of the FFT (modulo X^m-i) - */ -EXPORT void cplx_fft_avx2_fma(const CPLX_FFT_PRECOMP* precomp, void* d) { - const uint32_t m = precomp->m; - const D4MEM* omg = (D4MEM*)precomp->powomegas; - if (m <= 1) return; - if (m <= 8) return cplx_fft_avx2_fma_bfs_2(d, &omg, m); - if (m <= 2048) return cplx_fft_avx2_fma_bfs_16(d, &omg, m); - cplx_fft_avx2_fma_rec_16(d, &omg, m); -}
D spqlios/lib/spqlios/cplx/cplx_fft_avx512.c

@@ -1,453 +0,0 @@

-#include <immintrin.h> - -#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -typedef double D2MEM[2]; -typedef double D4MEM[4]; -typedef double D8MEM[8]; - -EXPORT void cplx_fftvec_addmul_avx512(const CPLX_FFTVEC_ADDMUL_PRECOMP* precomp, void* r, const void* a, - const void* b) { - const uint32_t m = precomp->m; - const D8MEM* aa = (D8MEM*)a; - const D8MEM* bb = (D8MEM*)b; - D8MEM* rr = (D8MEM*)r; - const D8MEM* const aend = aa + (m >> 2); - do { - /* -BEGIN_TEMPLATE -const __m512d ari% = _mm512_loadu_pd(aa[%]); -const __m512d bri% = _mm512_loadu_pd(bb[%]); -const __m512d rri% = _mm512_loadu_pd(rr[%]); -const __m512d bir% = _mm512_shuffle_pd(bri%,bri%, 0b01010101); -const __m512d aii% = _mm512_shuffle_pd(ari%,ari%, 0b11111111); -const __m512d pro% = _mm512_fmaddsub_pd(aii%,bir%,rri%); -const __m512d arr% = _mm512_shuffle_pd(ari%,ari%, 0b00000000); -const __m512d res% = _mm512_fmaddsub_pd(arr%,bri%,pro%); -_mm512_storeu_pd(rr[%],res%); -rr += @; // ONCE -aa += @; // ONCE -bb += @; // ONCE -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 2 -const __m512d ari0 = _mm512_loadu_pd(aa[0]); -const __m512d ari1 = _mm512_loadu_pd(aa[1]); -const __m512d bri0 = _mm512_loadu_pd(bb[0]); -const __m512d bri1 = _mm512_loadu_pd(bb[1]); -const __m512d rri0 = _mm512_loadu_pd(rr[0]); -const __m512d rri1 = _mm512_loadu_pd(rr[1]); -const __m512d bir0 = _mm512_shuffle_pd(bri0,bri0, 0b01010101); -const __m512d bir1 = _mm512_shuffle_pd(bri1,bri1, 0b01010101); -const __m512d aii0 = _mm512_shuffle_pd(ari0,ari0, 0b11111111); -const __m512d aii1 = _mm512_shuffle_pd(ari1,ari1, 0b11111111); -const __m512d pro0 = _mm512_fmaddsub_pd(aii0,bir0,rri0); -const __m512d pro1 = _mm512_fmaddsub_pd(aii1,bir1,rri1); -const __m512d arr0 = _mm512_shuffle_pd(ari0,ari0, 0b00000000); -const __m512d arr1 = _mm512_shuffle_pd(ari1,ari1, 0b00000000); -const __m512d res0 = _mm512_fmaddsub_pd(arr0,bri0,pro0); -const __m512d res1 = _mm512_fmaddsub_pd(arr1,bri1,pro1); -_mm512_storeu_pd(rr[0],res0); -_mm512_storeu_pd(rr[1],res1); -rr += 2; // ONCE -aa += 2; // ONCE -bb += 2; // ONCE - // END_INTERLEAVE - } while (aa < aend); -} - -#if 0 -EXPORT void cplx_fftvec_mul_fma(uint32_t m, void* r, const void* a, const void* b) { - const double(*aa)[4] = (double(*)[4])a; - const double(*bb)[4] = (double(*)[4])b; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d bir% = _mm256_shuffle_pd(bri%,bri%, 5); // conj of b -const __m256d aii% = _mm256_shuffle_pd(ari%,ari%, 15); // im of a -const __m256d pro% = _mm256_mul_pd(aii%,bir%); -const __m256d arr% = _mm256_shuffle_pd(ari%,ari%, 0); // rr of a -const __m256d res% = _mm256_fmaddsub_pd(arr%,bri%,pro%); -_mm256_storeu_pd(rr[%],res%); -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 -const __m256d ari0 = _mm256_loadu_pd(aa[0]); -const __m256d ari1 = _mm256_loadu_pd(aa[1]); -const __m256d ari2 = _mm256_loadu_pd(aa[2]); -const __m256d ari3 = _mm256_loadu_pd(aa[3]); -const __m256d bri0 = _mm256_loadu_pd(bb[0]); -const __m256d bri1 = _mm256_loadu_pd(bb[1]); -const __m256d bri2 = _mm256_loadu_pd(bb[2]); -const __m256d bri3 = _mm256_loadu_pd(bb[3]); -const __m256d bir0 = _mm256_shuffle_pd(bri0,bri0, 5); // conj of b -const __m256d bir1 = _mm256_shuffle_pd(bri1,bri1, 5); // conj of b -const __m256d bir2 = _mm256_shuffle_pd(bri2,bri2, 5); // conj of b -const __m256d bir3 = _mm256_shuffle_pd(bri3,bri3, 5); // conj of b -const __m256d aii0 = _mm256_shuffle_pd(ari0,ari0, 15); // im of a -const __m256d aii1 = _mm256_shuffle_pd(ari1,ari1, 15); // im of a -const __m256d aii2 = _mm256_shuffle_pd(ari2,ari2, 15); // im of a -const __m256d aii3 = _mm256_shuffle_pd(ari3,ari3, 15); // im of a -const __m256d pro0 = _mm256_mul_pd(aii0,bir0); -const __m256d pro1 = _mm256_mul_pd(aii1,bir1); -const __m256d pro2 = _mm256_mul_pd(aii2,bir2); -const __m256d pro3 = _mm256_mul_pd(aii3,bir3); -const __m256d arr0 = _mm256_shuffle_pd(ari0,ari0, 0); // rr of a -const __m256d arr1 = _mm256_shuffle_pd(ari1,ari1, 0); // rr of a -const __m256d arr2 = _mm256_shuffle_pd(ari2,ari2, 0); // rr of a -const __m256d arr3 = _mm256_shuffle_pd(ari3,ari3, 0); // rr of a -const __m256d res0 = _mm256_fmaddsub_pd(arr0,bri0,pro0); -const __m256d res1 = _mm256_fmaddsub_pd(arr1,bri1,pro1); -const __m256d res2 = _mm256_fmaddsub_pd(arr2,bri2,pro2); -const __m256d res3 = _mm256_fmaddsub_pd(arr3,bri3,pro3); -_mm256_storeu_pd(rr[0],res0); -_mm256_storeu_pd(rr[1],res1); -_mm256_storeu_pd(rr[2],res2); -_mm256_storeu_pd(rr[3],res3); - // END_INTERLEAVE - rr += 4; - aa += 4; - bb += 4; - } while (aa < aend); -} - -EXPORT void cplx_fftvec_add_fma(uint32_t m, void* r, const void* a, const void* b) { - const double(*aa)[4] = (double(*)[4])a; - const double(*bb)[4] = (double(*)[4])b; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d res% = _mm256_add_pd(ari%,bri%); -_mm256_storeu_pd(rr[%],res%); -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 -const __m256d ari0 = _mm256_loadu_pd(aa[0]); -const __m256d ari1 = _mm256_loadu_pd(aa[1]); -const __m256d ari2 = _mm256_loadu_pd(aa[2]); -const __m256d ari3 = _mm256_loadu_pd(aa[3]); -const __m256d bri0 = _mm256_loadu_pd(bb[0]); -const __m256d bri1 = _mm256_loadu_pd(bb[1]); -const __m256d bri2 = _mm256_loadu_pd(bb[2]); -const __m256d bri3 = _mm256_loadu_pd(bb[3]); -const __m256d res0 = _mm256_add_pd(ari0,bri0); -const __m256d res1 = _mm256_add_pd(ari1,bri1); -const __m256d res2 = _mm256_add_pd(ari2,bri2); -const __m256d res3 = _mm256_add_pd(ari3,bri3); -_mm256_storeu_pd(rr[0],res0); -_mm256_storeu_pd(rr[1],res1); -_mm256_storeu_pd(rr[2],res2); -_mm256_storeu_pd(rr[3],res3); - // END_INTERLEAVE - rr += 4; - aa += 4; - bb += 4; - } while (aa < aend); -} - -EXPORT void cplx_fftvec_sub2_to_fma(uint32_t m, void* r, const void* a, const void* b) { - const double(*aa)[4] = (double(*)[4])a; - const double(*bb)[4] = (double(*)[4])b; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d sum% = _mm256_add_pd(ari%,bri%); -const __m256d rri% = _mm256_loadu_pd(rr[%]); -const __m256d res% = _mm256_sub_pd(rri%,sum%); -_mm256_storeu_pd(rr[%],res%); -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 -const __m256d ari0 = _mm256_loadu_pd(aa[0]); -const __m256d ari1 = _mm256_loadu_pd(aa[1]); -const __m256d ari2 = _mm256_loadu_pd(aa[2]); -const __m256d ari3 = _mm256_loadu_pd(aa[3]); -const __m256d bri0 = _mm256_loadu_pd(bb[0]); -const __m256d bri1 = _mm256_loadu_pd(bb[1]); -const __m256d bri2 = _mm256_loadu_pd(bb[2]); -const __m256d bri3 = _mm256_loadu_pd(bb[3]); -const __m256d sum0 = _mm256_add_pd(ari0,bri0); -const __m256d sum1 = _mm256_add_pd(ari1,bri1); -const __m256d sum2 = _mm256_add_pd(ari2,bri2); -const __m256d sum3 = _mm256_add_pd(ari3,bri3); -const __m256d rri0 = _mm256_loadu_pd(rr[0]); -const __m256d rri1 = _mm256_loadu_pd(rr[1]); -const __m256d rri2 = _mm256_loadu_pd(rr[2]); -const __m256d rri3 = _mm256_loadu_pd(rr[3]); -const __m256d res0 = _mm256_sub_pd(rri0,sum0); -const __m256d res1 = _mm256_sub_pd(rri1,sum1); -const __m256d res2 = _mm256_sub_pd(rri2,sum2); -const __m256d res3 = _mm256_sub_pd(rri3,sum3); -_mm256_storeu_pd(rr[0],res0); -_mm256_storeu_pd(rr[1],res1); -_mm256_storeu_pd(rr[2],res2); -_mm256_storeu_pd(rr[3],res3); - // END_INTERLEAVE - rr += 4; - aa += 4; - bb += 4; - } while (aa < aend); -} - -EXPORT void cplx_fftvec_copy_fma(uint32_t m, void* r, const void* a) { - const double(*aa)[4] = (double(*)[4])a; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -_mm256_storeu_pd(rr[%],ari%); -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 -const __m256d ari0 = _mm256_loadu_pd(aa[0]); -const __m256d ari1 = _mm256_loadu_pd(aa[1]); -const __m256d ari2 = _mm256_loadu_pd(aa[2]); -const __m256d ari3 = _mm256_loadu_pd(aa[3]); -_mm256_storeu_pd(rr[0],ari0); -_mm256_storeu_pd(rr[1],ari1); -_mm256_storeu_pd(rr[2],ari2); -_mm256_storeu_pd(rr[3],ari3); - // END_INTERLEAVE - rr += 4; - aa += 4; - } while (aa < aend); -} - -EXPORT void cplx_fftvec_twiddle_fma(uint32_t m, void* a, void* b, const void* omg) { - double(*aa)[4] = (double(*)[4])a; - double(*bb)[4] = (double(*)[4])b; - const double(*const aend)[4] = aa + (m >> 1); - const __m256d om = _mm256_loadu_pd(omg); - const __m256d omrr = _mm256_shuffle_pd(om, om, 0); - const __m256d omii = _mm256_shuffle_pd(om, om, 15); - do { - /* -BEGIN_TEMPLATE -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d bir% = _mm256_shuffle_pd(bri%,bri%,5); -__m256d p% = _mm256_mul_pd(bir%,omii); -p% = _mm256_fmaddsub_pd(bri%,omrr,p%); -const __m256d ari% = _mm256_loadu_pd(aa[%]); -_mm256_storeu_pd(aa[%],_mm256_add_pd(ari%,p%)); -_mm256_storeu_pd(bb[%],_mm256_sub_pd(ari%,p%)); -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 -const __m256d bri0 = _mm256_loadu_pd(bb[0]); -const __m256d bri1 = _mm256_loadu_pd(bb[1]); -const __m256d bri2 = _mm256_loadu_pd(bb[2]); -const __m256d bri3 = _mm256_loadu_pd(bb[3]); -const __m256d bir0 = _mm256_shuffle_pd(bri0,bri0,5); -const __m256d bir1 = _mm256_shuffle_pd(bri1,bri1,5); -const __m256d bir2 = _mm256_shuffle_pd(bri2,bri2,5); -const __m256d bir3 = _mm256_shuffle_pd(bri3,bri3,5); -__m256d p0 = _mm256_mul_pd(bir0,omii); -__m256d p1 = _mm256_mul_pd(bir1,omii); -__m256d p2 = _mm256_mul_pd(bir2,omii); -__m256d p3 = _mm256_mul_pd(bir3,omii); -p0 = _mm256_fmaddsub_pd(bri0,omrr,p0); -p1 = _mm256_fmaddsub_pd(bri1,omrr,p1); -p2 = _mm256_fmaddsub_pd(bri2,omrr,p2); -p3 = _mm256_fmaddsub_pd(bri3,omrr,p3); -const __m256d ari0 = _mm256_loadu_pd(aa[0]); -const __m256d ari1 = _mm256_loadu_pd(aa[1]); -const __m256d ari2 = _mm256_loadu_pd(aa[2]); -const __m256d ari3 = _mm256_loadu_pd(aa[3]); -_mm256_storeu_pd(aa[0],_mm256_add_pd(ari0,p0)); -_mm256_storeu_pd(aa[1],_mm256_add_pd(ari1,p1)); -_mm256_storeu_pd(aa[2],_mm256_add_pd(ari2,p2)); -_mm256_storeu_pd(aa[3],_mm256_add_pd(ari3,p3)); -_mm256_storeu_pd(bb[0],_mm256_sub_pd(ari0,p0)); -_mm256_storeu_pd(bb[1],_mm256_sub_pd(ari1,p1)); -_mm256_storeu_pd(bb[2],_mm256_sub_pd(ari2,p2)); -_mm256_storeu_pd(bb[3],_mm256_sub_pd(ari3,p3)); - // END_INTERLEAVE - bb += 4; - aa += 4; - } while (aa < aend); -} -#endif - -EXPORT void cplx_fftvec_twiddle_avx512(const CPLX_FFTVEC_TWIDDLE_PRECOMP* precomp, void* a, void* b, const void* omg) { - const uint32_t m = precomp->m; - D8MEM* aa = (D8MEM*)a; - D8MEM* bb = (D8MEM*)b; - D8MEM* const aend = aa + (m >> 2); - const __m512d om = _mm512_broadcast_f64x4(_mm256_loadu_pd(omg)); - const __m512d omrr = _mm512_shuffle_pd(om, om, 0b00000000); - const __m512d omii = _mm512_shuffle_pd(om, om, 0b11111111); - do { -/* -BEGIN_TEMPLATE -const __m512d bri% = _mm512_loadu_pd(bb[%]); -const __m512d bir% = _mm512_shuffle_pd(bri%,bri%,0b10011001); -__m512d p% = _mm512_mul_pd(bir%,omii); -p% = _mm512_fmaddsub_pd(bri%,omrr,p%); -const __m512d ari% = _mm512_loadu_pd(aa[%]); -_mm512_storeu_pd(aa[%],_mm512_add_pd(ari%,p%)); -_mm512_storeu_pd(bb[%],_mm512_sub_pd(ari%,p%)); -bb += @; // ONCE -aa += @; // ONCE -END_TEMPLATE - */ -// BEGIN_INTERLEAVE 4 -const __m512d bri0 = _mm512_loadu_pd(bb[0]); -const __m512d bri1 = _mm512_loadu_pd(bb[1]); -const __m512d bri2 = _mm512_loadu_pd(bb[2]); -const __m512d bri3 = _mm512_loadu_pd(bb[3]); -const __m512d bir0 = _mm512_shuffle_pd(bri0,bri0,0b10011001); -const __m512d bir1 = _mm512_shuffle_pd(bri1,bri1,0b10011001); -const __m512d bir2 = _mm512_shuffle_pd(bri2,bri2,0b10011001); -const __m512d bir3 = _mm512_shuffle_pd(bri3,bri3,0b10011001); -__m512d p0 = _mm512_mul_pd(bir0,omii); -__m512d p1 = _mm512_mul_pd(bir1,omii); -__m512d p2 = _mm512_mul_pd(bir2,omii); -__m512d p3 = _mm512_mul_pd(bir3,omii); -p0 = _mm512_fmaddsub_pd(bri0,omrr,p0); -p1 = _mm512_fmaddsub_pd(bri1,omrr,p1); -p2 = _mm512_fmaddsub_pd(bri2,omrr,p2); -p3 = _mm512_fmaddsub_pd(bri3,omrr,p3); -const __m512d ari0 = _mm512_loadu_pd(aa[0]); -const __m512d ari1 = _mm512_loadu_pd(aa[1]); -const __m512d ari2 = _mm512_loadu_pd(aa[2]); -const __m512d ari3 = _mm512_loadu_pd(aa[3]); -_mm512_storeu_pd(aa[0],_mm512_add_pd(ari0,p0)); -_mm512_storeu_pd(aa[1],_mm512_add_pd(ari1,p1)); -_mm512_storeu_pd(aa[2],_mm512_add_pd(ari2,p2)); -_mm512_storeu_pd(aa[3],_mm512_add_pd(ari3,p3)); -_mm512_storeu_pd(bb[0], _mm512_sub_pd(ari0, p0)); -_mm512_storeu_pd(bb[1], _mm512_sub_pd(ari1, p1)); -_mm512_storeu_pd(bb[2], _mm512_sub_pd(ari2, p2)); -_mm512_storeu_pd(bb[3], _mm512_sub_pd(ari3, p3)); -bb += 4; // ONCE -aa += 4; // ONCE - // END_INTERLEAVE - } while (aa < aend); -} - -EXPORT void cplx_fftvec_bitwiddle_avx512(const CPLX_FFTVEC_BITWIDDLE_PRECOMP* precomp, void* a, uint64_t slicea, - const void* omg) { - const uint32_t m = precomp->m; - const uint64_t OFFSET = slicea / sizeof(D8MEM); - D8MEM* aa = (D8MEM*)a; - const D8MEM* aend = aa + (m >> 2); - const __m512d om = _mm512_broadcast_f64x4(_mm256_loadu_pd(omg)); - const __m512d om1rr = _mm512_shuffle_pd(om, om, 0); - const __m512d om1ii = _mm512_shuffle_pd(om, om, 15); - const __m512d om2rr = _mm512_shuffle_pd(om, om, 0); - const __m512d om2ii = _mm512_shuffle_pd(om, om, 0); - const __m512d om3rr = _mm512_shuffle_pd(om, om, 15); - const __m512d om3ii = _mm512_shuffle_pd(om, om, 15); - do { -/* -BEGIN_TEMPLATE -__m512d ari% = _mm512_loadu_pd(aa[%]); -__m512d bri% = _mm512_loadu_pd((aa+OFFSET)[%]); -__m512d cri% = _mm512_loadu_pd((aa+2*OFFSET)[%]); -__m512d dri% = _mm512_loadu_pd((aa+3*OFFSET)[%]); -__m512d pa% = _mm512_shuffle_pd(cri%,cri%,5); -__m512d pb% = _mm512_shuffle_pd(dri%,dri%,5); -pa% = _mm512_mul_pd(pa%,om1ii); -pb% = _mm512_mul_pd(pb%,om1ii); -pa% = _mm512_fmaddsub_pd(cri%,om1rr,pa%); -pb% = _mm512_fmaddsub_pd(dri%,om1rr,pb%); -cri% = _mm512_sub_pd(ari%,pa%); -dri% = _mm512_sub_pd(bri%,pb%); -ari% = _mm512_add_pd(ari%,pa%); -bri% = _mm512_add_pd(bri%,pb%); -pa% = _mm512_shuffle_pd(bri%,bri%,5); -pb% = _mm512_shuffle_pd(dri%,dri%,5); -pa% = _mm512_mul_pd(pa%,om2ii); -pb% = _mm512_mul_pd(pb%,om3ii); -pa% = _mm512_fmaddsub_pd(bri%,om2rr,pa%); -pb% = _mm512_fmaddsub_pd(dri%,om3rr,pb%); -bri% = _mm512_sub_pd(ari%,pa%); -dri% = _mm512_sub_pd(cri%,pb%); -ari% = _mm512_add_pd(ari%,pa%); -cri% = _mm512_add_pd(cri%,pb%); -_mm512_storeu_pd(aa[%], ari%); -_mm512_storeu_pd((aa+OFFSET)[%],bri%); -_mm512_storeu_pd((aa+2*OFFSET)[%],cri%); -_mm512_storeu_pd((aa+3*OFFSET)[%],dri%); -aa += @; // ONCE -END_TEMPLATE - */ -// BEGIN_INTERLEAVE 2 -__m512d ari0 = _mm512_loadu_pd(aa[0]); -__m512d ari1 = _mm512_loadu_pd(aa[1]); -__m512d bri0 = _mm512_loadu_pd((aa+OFFSET)[0]); -__m512d bri1 = _mm512_loadu_pd((aa+OFFSET)[1]); -__m512d cri0 = _mm512_loadu_pd((aa+2*OFFSET)[0]); -__m512d cri1 = _mm512_loadu_pd((aa+2*OFFSET)[1]); -__m512d dri0 = _mm512_loadu_pd((aa+3*OFFSET)[0]); -__m512d dri1 = _mm512_loadu_pd((aa+3*OFFSET)[1]); -__m512d pa0 = _mm512_shuffle_pd(cri0,cri0,5); -__m512d pa1 = _mm512_shuffle_pd(cri1,cri1,5); -__m512d pb0 = _mm512_shuffle_pd(dri0,dri0,5); -__m512d pb1 = _mm512_shuffle_pd(dri1,dri1,5); -pa0 = _mm512_mul_pd(pa0,om1ii); -pa1 = _mm512_mul_pd(pa1,om1ii); -pb0 = _mm512_mul_pd(pb0,om1ii); -pb1 = _mm512_mul_pd(pb1,om1ii); -pa0 = _mm512_fmaddsub_pd(cri0,om1rr,pa0); -pa1 = _mm512_fmaddsub_pd(cri1,om1rr,pa1); -pb0 = _mm512_fmaddsub_pd(dri0,om1rr,pb0); -pb1 = _mm512_fmaddsub_pd(dri1,om1rr,pb1); -cri0 = _mm512_sub_pd(ari0,pa0); -cri1 = _mm512_sub_pd(ari1,pa1); -dri0 = _mm512_sub_pd(bri0,pb0); -dri1 = _mm512_sub_pd(bri1,pb1); -ari0 = _mm512_add_pd(ari0,pa0); -ari1 = _mm512_add_pd(ari1,pa1); -bri0 = _mm512_add_pd(bri0,pb0); -bri1 = _mm512_add_pd(bri1,pb1); -pa0 = _mm512_shuffle_pd(bri0,bri0,5); -pa1 = _mm512_shuffle_pd(bri1,bri1,5); -pb0 = _mm512_shuffle_pd(dri0,dri0,5); -pb1 = _mm512_shuffle_pd(dri1,dri1,5); -pa0 = _mm512_mul_pd(pa0,om2ii); -pa1 = _mm512_mul_pd(pa1,om2ii); -pb0 = _mm512_mul_pd(pb0,om3ii); -pb1 = _mm512_mul_pd(pb1,om3ii); -pa0 = _mm512_fmaddsub_pd(bri0,om2rr,pa0); -pa1 = _mm512_fmaddsub_pd(bri1,om2rr,pa1); -pb0 = _mm512_fmaddsub_pd(dri0,om3rr,pb0); -pb1 = _mm512_fmaddsub_pd(dri1,om3rr,pb1); -bri0 = _mm512_sub_pd(ari0,pa0); -bri1 = _mm512_sub_pd(ari1,pa1); -dri0 = _mm512_sub_pd(cri0,pb0); -dri1 = _mm512_sub_pd(cri1,pb1); -ari0 = _mm512_add_pd(ari0,pa0); -ari1 = _mm512_add_pd(ari1,pa1); -cri0 = _mm512_add_pd(cri0,pb0); -cri1 = _mm512_add_pd(cri1,pb1); -_mm512_storeu_pd(aa[0], ari0); -_mm512_storeu_pd(aa[1], ari1); -_mm512_storeu_pd((aa+OFFSET)[0],bri0); -_mm512_storeu_pd((aa+OFFSET)[1],bri1); -_mm512_storeu_pd((aa+2*OFFSET)[0],cri0); -_mm512_storeu_pd((aa+2*OFFSET)[1],cri1); -_mm512_storeu_pd((aa+3*OFFSET)[0],dri0); -_mm512_storeu_pd((aa+3*OFFSET)[1],dri1); -aa += 2; // ONCE - // END_INTERLEAVE - } while (aa < aend); - _mm256_zeroupper(); -} - -
D spqlios/lib/spqlios/cplx/cplx_fft_internal.h

@@ -1,123 +0,0 @@

-#ifndef SPQLIOS_CPLX_FFT_INTERNAL_H -#define SPQLIOS_CPLX_FFT_INTERNAL_H - -#include "cplx_fft.h" - -/** @brief a complex number contains two doubles real,imag */ -typedef double CPLX[2]; - -EXPORT void cplx_set(CPLX r, const CPLX a); -EXPORT void cplx_neg(CPLX r, const CPLX a); -EXPORT void cplx_add(CPLX r, const CPLX a, const CPLX b); -EXPORT void cplx_sub(CPLX r, const CPLX a, const CPLX b); -EXPORT void cplx_mul(CPLX r, const CPLX a, const CPLX b); - -/** - * @brief splits 2h evaluations of one polynomials into 2 times h evaluations of even/odd polynomial - * Input: Q_0(y),...,Q_{h-1}(y),Q_0(-y),...,Q_{h-1}(-y) - * Output: P_0(z),...,P_{h-1}(z),P_h(z),...,P_{2h-1}(z) - * where Q_i(X)=P_i(X^2)+X.P_{h+i}(X^2) and y^2 = z - * @param h number of "coefficients" h >= 1 - * @param data 2h complex coefficients interleaved and 256b aligned - * @param powom y represented as (yre,yim) - */ -EXPORT void cplx_split_fft_ref(int32_t h, CPLX* data, const CPLX powom); -EXPORT void cplx_bisplit_fft_ref(int32_t h, CPLX* data, const CPLX powom[2]); - -/** - * Input: Q(y),Q(-y) - * Output: P_0(z),P_1(z) - * where Q(X)=P_0(X^2)+X.P_1(X^2) and y^2 = z - * @param data 2 complexes coefficients interleaved and 256b aligned - * @param powom (z,-z) interleaved: (zre,zim,-zre,-zim) - */ -EXPORT void split_fft_last_ref(CPLX* data, const CPLX powom); - -EXPORT void cplx_ifft_naive(const uint32_t m, const double entry_pwr, CPLX* data); -EXPORT void cplx_ifft16_avx_fma(void* data, const void* omega); -EXPORT void cplx_ifft16_ref(void* data, const void* omega); - -/** - * @brief compute the ifft evaluations of P in place - * ifft(data) = ifft_rec(data, i); - * function ifft_rec(data, omega) { - * if #data = 1: return data - * let s = sqrt(omega) w. re(s)>0 - * let (u,v) = data - * return split_fft([ifft_rec(u, s), ifft_rec(v, -s)],s) - * } - * @param itables precomputed tables (contains all the powers of omega in the order they are used) - * @param data vector of m complexes (coeffs as input, evals as output) - */ -EXPORT void cplx_ifft_ref(const CPLX_IFFT_PRECOMP* itables, void* data); -EXPORT void cplx_ifft_avx2_fma(const CPLX_IFFT_PRECOMP* itables, void* data); -EXPORT void cplx_fft_naive(const uint32_t m, const double entry_pwr, CPLX* data); -EXPORT void cplx_fft16_avx_fma(void* data, const void* omega); -EXPORT void cplx_fft16_ref(void* data, const void* omega); - -/** - * @brief compute the fft evaluations of P in place - * fft(data) = fft_rec(data, i); - * function fft_rec(data, omega) { - * if #data = 1: return data - * let s = sqrt(omega) w. re(s)>0 - * let (u,v) = merge_fft(data, s) - * return [fft_rec(u, s), fft_rec(v, -s)] - * } - * @param tables precomputed tables (contains all the powers of omega in the order they are used) - * @param data vector of m complexes (coeffs as input, evals as output) - */ -EXPORT void cplx_fft_ref(const CPLX_FFT_PRECOMP* tables, void* data); -EXPORT void cplx_fft_avx2_fma(const CPLX_FFT_PRECOMP* tables, void* data); - -/** - * @brief merges 2 times h evaluations of even/odd polynomials into 2h evaluations of a sigle polynomial - * Input: P_0(z),...,P_{h-1}(z),P_h(z),...,P_{2h-1}(z) - * Output: Q_0(y),...,Q_{h-1}(y),Q_0(-y),...,Q_{h-1}(-y) - * where Q_i(X)=P_i(X^2)+X.P_{h+i}(X^2) and y^2 = z - * @param h number of "coefficients" h >= 1 - * @param data 2h complex coefficients interleaved and 256b aligned - * @param powom y represented as (yre,yim) - */ -EXPORT void cplx_twiddle_fft_ref(int32_t h, CPLX* data, const CPLX powom); - -EXPORT void citwiddle(CPLX a, CPLX b, const CPLX om); -EXPORT void ctwiddle(CPLX a, CPLX b, const CPLX om); -EXPORT void invctwiddle(CPLX a, CPLX b, const CPLX ombar); -EXPORT void invcitwiddle(CPLX a, CPLX b, const CPLX ombar); - -// CONVERSIONS - -/** @brief r = x from ZnX (coeffs as signed int32_t's ) to double */ -EXPORT void cplx_from_znx32_ref(const CPLX_FROM_ZNX32_PRECOMP* precomp, void* r, const int32_t* x); -EXPORT void cplx_from_znx32_avx2_fma(const CPLX_FROM_ZNX32_PRECOMP* precomp, void* r, const int32_t* x); -/** @brief r = x to ZnX (coeffs as signed int32_t's ) to double */ -EXPORT void cplx_to_znx32_ref(const CPLX_TO_ZNX32_PRECOMP* precomp, int32_t* r, const void* x); -EXPORT void cplx_to_znx32_avx2_fma(const CPLX_TO_ZNX32_PRECOMP* precomp, int32_t* r, const void* x); -/** @brief r = x mod 1 from TnX (coeffs as signed int32_t's) to double */ -EXPORT void cplx_from_tnx32_ref(const CPLX_FROM_TNX32_PRECOMP* precomp, void* r, const int32_t* x); -EXPORT void cplx_from_tnx32_avx2_fma(const CPLX_FROM_TNX32_PRECOMP* precomp, void* r, const int32_t* x); -/** @brief r = x mod 1 from TnX (coeffs as signed int32_t's) */ -EXPORT void cplx_to_tnx32_ref(const CPLX_TO_TNX32_PRECOMP* precomp, int32_t* x, const void* c); -EXPORT void cplx_to_tnx32_avx2_fma(const CPLX_TO_TNX32_PRECOMP* precomp, int32_t* x, const void* c); -/** @brief r = x from RnX (coeffs as doubles ) to double */ -EXPORT void cplx_from_rnx64_ref(const CPLX_FROM_RNX64_PRECOMP* precomp, void* r, const double* x); -EXPORT void cplx_from_rnx64_avx2_fma(const CPLX_FROM_RNX64_PRECOMP* precomp, void* r, const double* x); -/** @brief r = x to RnX (coeffs as doubles ) to double */ -EXPORT void cplx_to_rnx64_ref(const CPLX_TO_RNX64_PRECOMP* precomp, double* r, const void* x); -EXPORT void cplx_to_rnx64_avx2_fma(const CPLX_TO_RNX64_PRECOMP* precomp, double* r, const void* x); -/** @brief r = x to integers in RnX (coeffs as doubles ) to double */ -EXPORT void cplx_round_to_rnx64_ref(const CPLX_ROUND_TO_RNX64_PRECOMP* precomp, double* r, const void* x); -EXPORT void cplx_round_to_rnx64_avx2_fma(const CPLX_ROUND_TO_RNX64_PRECOMP* precomp, double* r, const void* x); - -// fftvec operations -/** @brief element-wise addmul r += ab */ -EXPORT void cplx_fftvec_addmul_ref(const CPLX_FFTVEC_ADDMUL_PRECOMP* precomp, void* r, const void* a, const void* b); -EXPORT void cplx_fftvec_addmul_fma(const CPLX_FFTVEC_ADDMUL_PRECOMP* tables, void* r, const void* a, const void* b); -EXPORT void cplx_fftvec_addmul_sse(const CPLX_FFTVEC_ADDMUL_PRECOMP* precomp, void* r, const void* a, const void* b); -EXPORT void cplx_fftvec_addmul_avx512(const CPLX_FFTVEC_ADDMUL_PRECOMP* precomp, void* r, const void* a, const void* b); -/** @brief element-wise mul r = ab */ -EXPORT void cplx_fftvec_mul_ref(const CPLX_FFTVEC_MUL_PRECOMP* tables, void* r, const void* a, const void* b); -EXPORT void cplx_fftvec_mul_fma(const CPLX_FFTVEC_MUL_PRECOMP* tables, void* r, const void* a, const void* b); - -#endif // SPQLIOS_CPLX_FFT_INTERNAL_H
D spqlios/lib/spqlios/cplx/cplx_fft_private.h

@@ -1,109 +0,0 @@

-#ifndef SPQLIOS_CPLX_FFT_PRIVATE_H -#define SPQLIOS_CPLX_FFT_PRIVATE_H - -#include "cplx_fft.h" - -typedef struct cplx_twiddle_precomp CPLX_FFTVEC_TWIDDLE_PRECOMP; -typedef struct cplx_bitwiddle_precomp CPLX_FFTVEC_BITWIDDLE_PRECOMP; - -typedef void (*IFFT_FUNCTION)(const CPLX_IFFT_PRECOMP*, void*); -typedef void (*FFT_FUNCTION)(const CPLX_FFT_PRECOMP*, void*); -// conversions -typedef void (*FROM_ZNX32_FUNCTION)(const CPLX_FROM_ZNX32_PRECOMP*, void*, const int32_t*); -typedef void (*TO_ZNX32_FUNCTION)(const CPLX_FROM_ZNX32_PRECOMP*, int32_t*, const void*); -typedef void (*FROM_TNX32_FUNCTION)(const CPLX_FROM_TNX32_PRECOMP*, void*, const int32_t*); -typedef void (*TO_TNX32_FUNCTION)(const CPLX_TO_TNX32_PRECOMP*, int32_t*, const void*); -typedef void (*FROM_RNX64_FUNCTION)(const CPLX_FROM_RNX64_PRECOMP* precomp, void* r, const double* x); -typedef void (*TO_RNX64_FUNCTION)(const CPLX_TO_RNX64_PRECOMP* precomp, double* r, const void* x); -typedef void (*ROUND_TO_RNX64_FUNCTION)(const CPLX_ROUND_TO_RNX64_PRECOMP* precomp, double* r, const void* x); -// fftvec operations -typedef void (*FFTVEC_MUL_FUNCTION)(const CPLX_FFTVEC_MUL_PRECOMP*, void*, const void*, const void*); -typedef void (*FFTVEC_ADDMUL_FUNCTION)(const CPLX_FFTVEC_ADDMUL_PRECOMP*, void*, const void*, const void*); - -typedef void (*FFTVEC_TWIDDLE_FUNCTION)(const CPLX_FFTVEC_TWIDDLE_PRECOMP*, void*, const void*, const void*); -typedef void (*FFTVEC_BITWIDDLE_FUNCTION)(const CPLX_FFTVEC_BITWIDDLE_PRECOMP*, void*, uint64_t, const void*); - -struct cplx_ifft_precomp { - IFFT_FUNCTION function; - int64_t m; - uint64_t buf_size; - double* powomegas; - void* aligned_buffers; -}; - -struct cplx_fft_precomp { - FFT_FUNCTION function; - int64_t m; - uint64_t buf_size; - double* powomegas; - void* aligned_buffers; -}; - -struct cplx_from_znx32_precomp { - FROM_ZNX32_FUNCTION function; - int64_t m; -}; - -struct cplx_to_znx32_precomp { - TO_ZNX32_FUNCTION function; - int64_t m; - double divisor; -}; - -struct cplx_from_tnx32_precomp { - FROM_TNX32_FUNCTION function; - int64_t m; -}; - -struct cplx_to_tnx32_precomp { - TO_TNX32_FUNCTION function; - int64_t m; - double divisor; -}; - -struct cplx_from_rnx64_precomp { - FROM_RNX64_FUNCTION function; - int64_t m; -}; - -struct cplx_to_rnx64_precomp { - TO_RNX64_FUNCTION function; - int64_t m; - double divisor; -}; - -struct cplx_round_to_rnx64_precomp { - ROUND_TO_RNX64_FUNCTION function; - int64_t m; - double divisor; - uint32_t log2bound; -}; - -typedef struct cplx_mul_precomp { - FFTVEC_MUL_FUNCTION function; - int64_t m; -} CPLX_FFTVEC_MUL_PRECOMP; - -typedef struct cplx_addmul_precomp { - FFTVEC_ADDMUL_FUNCTION function; - int64_t m; -} CPLX_FFTVEC_ADDMUL_PRECOMP; - -struct cplx_twiddle_precomp { - FFTVEC_TWIDDLE_FUNCTION function; - int64_t m; - }; - -struct cplx_bitwiddle_precomp { - FFTVEC_BITWIDDLE_FUNCTION function; - int64_t m; -}; - -EXPORT void cplx_fftvec_twiddle_fma(const CPLX_FFTVEC_TWIDDLE_PRECOMP* tables, void* a, void* b, const void* om); -EXPORT void cplx_fftvec_twiddle_avx512(const CPLX_FFTVEC_TWIDDLE_PRECOMP* tables, void* a, void* b, const void* om); -EXPORT void cplx_fftvec_bitwiddle_fma(const CPLX_FFTVEC_BITWIDDLE_PRECOMP* tables, void* a, uint64_t slice, - const void* om); -EXPORT void cplx_fftvec_bitwiddle_avx512(const CPLX_FFTVEC_BITWIDDLE_PRECOMP* tables, void* a, uint64_t slice, - const void* om); - -#endif // SPQLIOS_CPLX_FFT_PRIVATE_H
D spqlios/lib/spqlios/cplx/cplx_fft_ref.c

@@ -1,367 +0,0 @@

-#include <memory.h> -#include <stdio.h> - -#include "../commons_private.h" -#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -/** @brief (a,b) <- (a+omega.b,a-omega.b) */ -void ctwiddle(CPLX a, CPLX b, const CPLX om) { - double re = om[0] * b[0] - om[1] * b[1]; - double im = om[0] * b[1] + om[1] * b[0]; - b[0] = a[0] - re; - b[1] = a[1] - im; - a[0] += re; - a[1] += im; -} - -/** @brief (a,b) <- (a+i.omega.b,a-i.omega.b) */ -void citwiddle(CPLX a, CPLX b, const CPLX om) { - double re = -om[1] * b[0] - om[0] * b[1]; - double im = -om[1] * b[1] + om[0] * b[0]; - b[0] = a[0] - re; - b[1] = a[1] - im; - a[0] += re; - a[1] += im; -} - -/** - * @brief FFT modulo X^16-omega^2 (in registers) - * @param data contains 16 complexes - * @param omega 8 complexes in this order: - * omega,alpha,beta,j.beta,gamma,j.gamma,k.gamma,kj.gamma - * alpha = sqrt(omega), beta = sqrt(alpha), gamma = sqrt(beta) - * j = sqrt(i), k=sqrt(j) - */ -void cplx_fft16_ref(void* data, const void* omega) { - CPLX* d = data; - const CPLX* om = omega; - // first pass - for (uint64_t i = 0; i < 8; ++i) { - ctwiddle(d[0 + i], d[8 + i], om[0]); - } - // - ctwiddle(d[0], d[4], om[1]); - ctwiddle(d[1], d[5], om[1]); - ctwiddle(d[2], d[6], om[1]); - ctwiddle(d[3], d[7], om[1]); - citwiddle(d[8], d[12], om[1]); - citwiddle(d[9], d[13], om[1]); - citwiddle(d[10], d[14], om[1]); - citwiddle(d[11], d[15], om[1]); - // - ctwiddle(d[0], d[2], om[2]); - ctwiddle(d[1], d[3], om[2]); - citwiddle(d[4], d[6], om[2]); - citwiddle(d[5], d[7], om[2]); - ctwiddle(d[8], d[10], om[3]); - ctwiddle(d[9], d[11], om[3]); - citwiddle(d[12], d[14], om[3]); - citwiddle(d[13], d[15], om[3]); - // - ctwiddle(d[0], d[1], om[4]); - citwiddle(d[2], d[3], om[4]); - ctwiddle(d[4], d[5], om[5]); - citwiddle(d[6], d[7], om[5]); - ctwiddle(d[8], d[9], om[6]); - citwiddle(d[10], d[11], om[6]); - ctwiddle(d[12], d[13], om[7]); - citwiddle(d[14], d[15], om[7]); -} - -double cos_2pix(double x) { return m_accurate_cos(2 * M_PI * x); } -double sin_2pix(double x) { return m_accurate_sin(2 * M_PI * x); } -void cplx_set_e2pix(CPLX res, double x) { - res[0] = cos_2pix(x); - res[1] = sin_2pix(x); -} - -void cplx_fft16_precomp(const double entry_pwr, CPLX** omg) { - static const double j_pow = 1. / 8.; - static const double k_pow = 1. / 16.; - const double pom = entry_pwr / 2.; - const double pom_2 = entry_pwr / 4.; - const double pom_4 = entry_pwr / 8.; - const double pom_8 = entry_pwr / 16.; - cplx_set_e2pix((*omg)[0], pom); - cplx_set_e2pix((*omg)[1], pom_2); - cplx_set_e2pix((*omg)[2], pom_4); - cplx_set_e2pix((*omg)[3], pom_4 + j_pow); - cplx_set_e2pix((*omg)[4], pom_8); - cplx_set_e2pix((*omg)[5], pom_8 + j_pow); - cplx_set_e2pix((*omg)[6], pom_8 + k_pow); - cplx_set_e2pix((*omg)[7], pom_8 + j_pow + k_pow); - *omg += 8; -} - -/** - * @brief h twiddles-fft on the same omega - * (also called merge-fft)merges 2 times h evaluations of even/odd polynomials into 2h evaluations of a sigle polynomial - * Input: P_0(z),...,P_{h-1}(z),P_h(z),...,P_{2h-1}(z) - * Output: Q_0(y),...,Q_{h-1}(y),Q_0(-y),...,Q_{h-1}(-y) - * where Q_i(X)=P_i(X^2)+X.P_{h+i}(X^2) and y^2 = z - * @param h number of "coefficients" h >= 1 - * @param data 2h complex coefficients interleaved and 256b aligned - * @param powom y represented as (yre,yim) - */ -void cplx_twiddle_fft_ref(int32_t h, CPLX* data, const CPLX powom) { - CPLX* d0 = data; - CPLX* d1 = data + h; - for (uint64_t i = 0; i < h; ++i) { - ctwiddle(d0[i], d1[i], powom); - } -} - -void cplx_bitwiddle_fft_ref(int32_t h, CPLX* data, const CPLX powom[2]) { - CPLX* d0 = data; - CPLX* d1 = data + h; - CPLX* d2 = data + 2*h; - CPLX* d3 = data + 3*h; - for (uint64_t i = 0; i < h; ++i) { - ctwiddle(d0[i], d2[i], powom[0]); - ctwiddle(d1[i], d3[i], powom[0]); - } - for (uint64_t i = 0; i < h; ++i) { - ctwiddle(d0[i], d1[i], powom[1]); - citwiddle(d2[i], d3[i], powom[1]); - } -} - -/** - * Input: P_0(z),P_1(z) - * Output: Q(y),Q(-y) - * where Q(X)=P_0(X^2)+X.P_1(X^2) and y^2 = z - * @param data 2 complexes coefficients interleaved and 256b aligned - * @param powom (z,-z) interleaved: (zre,zim,-zre,-zim) - */ -void merge_fft_last_ref(CPLX* data, const CPLX powom) { - CPLX prod; - cplx_mul(prod, data[1], powom); - cplx_sub(data[1], data[0], prod); - cplx_add(data[0], data[0], prod); -} - -void cplx_fft_ref_bfs_2(CPLX* dat, const CPLX** omg, uint32_t m) { - CPLX* data = (CPLX*)dat; - CPLX* const dend = data + m; - for (int32_t h = m / 2; h >= 2; h >>= 1) { - for (CPLX* d = data; d < dend; d += 2 * h) { - if (memcmp((*omg)[0], (*omg)[1], 8) != 0) abort(); - cplx_twiddle_fft_ref(h, d, **omg); - *omg += 2; - } -#if 0 - printf("after merge %d: ", h); - for (uint64_t ii=0; ii<nn/2; ++ii) { - printf("%.6lf %.6lf ",data[ii][0],data[ii][1]); - } - printf("\n"); -#endif - } - for (CPLX* d = data; d < dend; d += 2) { - // TODO see if encoding changes - if ((*omg)[0][0] != -(*omg)[1][0]) abort(); - if ((*omg)[0][1] != -(*omg)[1][1]) abort(); - merge_fft_last_ref(d, **omg); - *omg += 2; - } -#if 0 - printf("after last: "); - for (uint64_t ii=0; ii<nn/2; ++ii) { - printf("%.6lf %.6lf ",data[ii][0],data[ii][1]); - } - printf("\n"); -#endif -} - -void cplx_fft_ref_bfs_16(CPLX* dat, const CPLX** omg, uint32_t m) { - CPLX* data = (CPLX*)dat; - CPLX* const dend = data + m; - uint32_t mm = m; - uint32_t log2m = log2(m); - if (log2m % 2 == 1) { - cplx_twiddle_fft_ref(mm/2, data, **omg); - *omg += 2; - mm >>= 1; - } - while(mm>16) { - uint32_t h = mm/4; - for (CPLX* d = data; d < dend; d += mm) { - cplx_bitwiddle_fft_ref(h, d, *omg); - *omg += 2; - } - mm=h; - } - for (CPLX* d = data; d < dend; d += 16) { - cplx_fft16_ref(d, *omg); - *omg += 8; - } -#if 0 - printf("after last: "); - for (uint64_t ii=0; ii<nn/2; ++ii) { - printf("%.6lf %.6lf ",data[ii][0],data[ii][1]); - } - printf("\n"); -#endif -} - -/** @brief fft modulo X^m-exp(i.2pi.entry+pwr) -- reference code */ -void cplx_fft_naive(const uint32_t m, const double entry_pwr, CPLX* data) { - if (m == 1) return; - const double pom = entry_pwr / 2.; - const uint32_t h = m / 2; - // apply the twiddle factors - CPLX cpom; - cplx_set_e2pix(cpom, pom); - for (uint64_t i = 0; i < h; ++i) { - ctwiddle(data[i], data[i + h], cpom); - } - // do the recursive calls - cplx_fft_naive(h, pom, data); - cplx_fft_naive(h, pom + 0.5, data + h); -} - -/** @brief fills omega for cplx_fft_bfs_16 modulo X^m-exp(i.2.pi.entry_pwr) */ -void fill_cplx_fft_omegas_bfs_16(const double entry_pwr, CPLX** omg, uint32_t m) { - uint32_t mm = m; - uint32_t log2m = log2(m); - double ss = entry_pwr; - if (log2m % 2 == 1) { - uint32_t h = mm / 2; - double pom = ss / 2.; - for (uint32_t i = 0; i < m / mm; i++) { - cplx_set_e2pix(omg[0][0], pom + fracrevbits(i) / 2.); - cplx_set(omg[0][1], omg[0][0]); - *omg += 2; - } - mm = h; - ss = pom; - } - while (mm>16) { - double pom = ss / 4.; - uint32_t h = mm / 4; - for (uint32_t i = 0; i < m / mm; i++) { - double om = pom + fracrevbits(i) / 4.; - cplx_set_e2pix(omg[0][0], 2. * om); - cplx_set_e2pix(omg[0][1], om); - *omg += 2; - } - mm = h; - ss = pom; - } - { - // mm=16 - for (uint32_t i = 0; i < m / 16; i++) { - cplx_fft16_precomp(ss + fracrevbits(i), omg); - } - } -} - -/** @brief fills omega for cplx_fft_bfs_2 modulo X^m-exp(i.2.pi.entry_pwr) */ -void fill_cplx_fft_omegas_bfs_2(const double entry_pwr, CPLX** omg, uint32_t m) { - double pom = entry_pwr / 2.; - for (int32_t h = m / 2; h >= 2; h >>= 1) { - for (uint32_t i = 0; i < m / (2 * h); i++) { - cplx_set_e2pix(omg[0][0], pom + fracrevbits(i) / 2.); - cplx_set(omg[0][1], omg[0][0]); - *omg += 2; - } - pom /= 2; - } - { - // h=1 - for (uint32_t i = 0; i < m / 2; i++) { - cplx_set_e2pix((*omg)[0], pom + fracrevbits(i) / 2.); - cplx_neg((*omg)[1], (*omg)[0]); - *omg += 2; - } - } -} - -/** @brief fills omega for cplx_fft_rec modulo X^m-exp(i.2.pi.entry_pwr) */ -void fill_cplx_fft_omegas_rec_16(const double entry_pwr, CPLX** omg, uint32_t m) { - // note that the cases below are for recursive calls only! - // externally, this function shall only be called with m>=4096 - if (m == 1) return; - if (m <= 8) return fill_cplx_fft_omegas_bfs_2(entry_pwr, omg, m); - if (m <= 2048) return fill_cplx_fft_omegas_bfs_16(entry_pwr, omg, m); - double pom = entry_pwr / 2.; - cplx_set_e2pix((*omg)[0], pom); - cplx_set_e2pix((*omg)[1], pom); - *omg += 2; - fill_cplx_fft_omegas_rec_16(pom, omg, m / 2); - fill_cplx_fft_omegas_rec_16(pom + 0.5, omg, m / 2); -} - -void cplx_fft_ref_rec_16(CPLX* dat, const CPLX** omg, uint32_t m) { - if (m == 1) return; - if (m <= 8) return cplx_fft_ref_bfs_2(dat, omg, m); - if (m <= 2048) return cplx_fft_ref_bfs_16(dat, omg, m); - const uint32_t h = m / 2; - if (memcmp((*omg)[0], (*omg)[1], 8) != 0) abort(); - cplx_twiddle_fft_ref(h, dat, **omg); - *omg += 2; - cplx_fft_ref_rec_16(dat, omg, h); - cplx_fft_ref_rec_16(dat + h, omg, h); -} - -void cplx_fft_ref(const CPLX_FFT_PRECOMP* precomp, void* d) { - CPLX* data = (CPLX*)d; - const int32_t m = precomp->m; - const CPLX* omg = (CPLX*)precomp->powomegas; - if (m == 1) return; - if (m <= 8) return cplx_fft_ref_bfs_2(data, &omg, m); - if (m <= 2048) return cplx_fft_ref_bfs_16(data, &omg, m); - cplx_fft_ref_rec_16(data, &omg, m); -} - -EXPORT CPLX_FFT_PRECOMP* new_cplx_fft_precomp(uint32_t m, uint32_t num_buffers) { - const uint64_t OMG_SPACE = ceilto64b((2 * m)* sizeof(CPLX)); - const uint64_t BUF_SIZE = ceilto64b(m * sizeof(CPLX)); - void* reps = malloc(sizeof(CPLX_FFT_PRECOMP) + 63 // padding - + OMG_SPACE // tables //TODO 16? - + num_buffers * BUF_SIZE // buffers - ); - uint64_t aligned_addr = ceilto64b((uint64_t)(reps) + sizeof(CPLX_FFT_PRECOMP)); - CPLX_FFT_PRECOMP* r = (CPLX_FFT_PRECOMP*)reps; - r->m = m; - r->buf_size = BUF_SIZE; - r->powomegas = (double*)aligned_addr; - r->aligned_buffers = (void*)(aligned_addr + OMG_SPACE); - // fill in powomegas - CPLX* omg = (CPLX*)r->powomegas; - if (m <= 8) { - fill_cplx_fft_omegas_bfs_2(0.25, &omg, m); - } else if (m <= 2048) { - fill_cplx_fft_omegas_bfs_16(0.25, &omg, m); - } else { - fill_cplx_fft_omegas_rec_16(0.25, &omg, m); - } - if (((uint64_t)omg) - aligned_addr > OMG_SPACE) abort(); - // dispatch the right implementation - { - if (m <= 4) { - // currently, we do not have any acceletated - // implementation for m<=4 - r->function = cplx_fft_ref; - } else if (CPU_SUPPORTS("fma")) { - r->function = cplx_fft_avx2_fma; - } else { - r->function = cplx_fft_ref; - } - } - return reps; -} - -EXPORT void* cplx_fft_precomp_get_buffer(const CPLX_FFT_PRECOMP* tables, uint32_t buffer_index) { - return (uint8_t *)tables->aligned_buffers + buffer_index * tables->buf_size; -} - -EXPORT void cplx_fft_simple(uint32_t m, void* data) { - static CPLX_FFT_PRECOMP* p[31] = {0}; - CPLX_FFT_PRECOMP** f = p + log2m(m); - if (!*f) *f = new_cplx_fft_precomp(m, 0); - (*f)->function(*f, data); -} - -EXPORT void cplx_fft(const CPLX_FFT_PRECOMP* tables, void* data) { tables->function(tables, data); }
D spqlios/lib/spqlios/cplx/cplx_fft_sse.c

@@ -1,310 +0,0 @@

-#include <immintrin.h> - -#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -typedef double D2MEM[2]; - -EXPORT void cplx_fftvec_addmul_sse(const CPLX_FFTVEC_ADDMUL_PRECOMP* precomp, void* r, const void* a, const void* b) { - const uint32_t m = precomp->m; - const D2MEM* aa = (D2MEM*)a; - const D2MEM* bb = (D2MEM*)b; - D2MEM* rr = (D2MEM*)r; - const D2MEM* const aend = aa + m; - do { - /* -BEGIN_TEMPLATE -const __m128d ari% = _mm_loadu_pd(aa[%]); -const __m128d bri% = _mm_loadu_pd(bb[%]); -const __m128d rri% = _mm_loadu_pd(rr[%]); -const __m128d bir% = _mm_shuffle_pd(bri%,bri%, 5); -const __m128d aii% = _mm_shuffle_pd(ari%,ari%, 15); -const __m128d pro% = _mm_fmaddsub_pd(aii%,bir%,rri%); -const __m128d arr% = _mm_shuffle_pd(ari%,ari%, 0); -const __m128d res% = _mm_fmaddsub_pd(arr%,bri%,pro%); -_mm_storeu_pd(rr[%],res%); -rr += @; // ONCE -aa += @; // ONCE -bb += @; // ONCE -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 2 - const __m128d ari0 = _mm_loadu_pd(aa[0]); - const __m128d ari1 = _mm_loadu_pd(aa[1]); - const __m128d bri0 = _mm_loadu_pd(bb[0]); - const __m128d bri1 = _mm_loadu_pd(bb[1]); - const __m128d rri0 = _mm_loadu_pd(rr[0]); - const __m128d rri1 = _mm_loadu_pd(rr[1]); - const __m128d bir0 = _mm_shuffle_pd(bri0, bri0, 0b01); - const __m128d bir1 = _mm_shuffle_pd(bri1, bri1, 0b01); - const __m128d aii0 = _mm_shuffle_pd(ari0, ari0, 0b11); - const __m128d aii1 = _mm_shuffle_pd(ari1, ari1, 0b11); - const __m128d pro0 = _mm_fmaddsub_pd(aii0, bir0, rri0); - const __m128d pro1 = _mm_fmaddsub_pd(aii1, bir1, rri1); - const __m128d arr0 = _mm_shuffle_pd(ari0, ari0, 0b00); - const __m128d arr1 = _mm_shuffle_pd(ari1, ari1, 0b00); - const __m128d res0 = _mm_fmaddsub_pd(arr0, bri0, pro0); - const __m128d res1 = _mm_fmaddsub_pd(arr1, bri1, pro1); - _mm_storeu_pd(rr[0], res0); - _mm_storeu_pd(rr[1], res1); - rr += 2; // ONCE - aa += 2; // ONCE - bb += 2; // ONCE - // END_INTERLEAVE - } while (aa < aend); -} - -#if 0 -EXPORT void cplx_fftvec_mul_fma(const CPLX_FFTVEC_MUL_PRECOMP* precomp, void* r, const void* a, const void* b) { - const uint32_t m = precomp->m; - const double(*aa)[4] = (double(*)[4])a; - const double(*bb)[4] = (double(*)[4])b; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d bir% = _mm256_shuffle_pd(bri%,bri%, 5); // conj of b -const __m256d aii% = _mm256_shuffle_pd(ari%,ari%, 15); // im of a -const __m256d pro% = _mm256_mul_pd(aii%,bir%); -const __m256d arr% = _mm256_shuffle_pd(ari%,ari%, 0); // rr of a -const __m256d res% = _mm256_fmaddsub_pd(arr%,bri%,pro%); -_mm256_storeu_pd(rr[%],res%); -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 -const __m256d ari0 = _mm256_loadu_pd(aa[0]); -const __m256d ari1 = _mm256_loadu_pd(aa[1]); -const __m256d ari2 = _mm256_loadu_pd(aa[2]); -const __m256d ari3 = _mm256_loadu_pd(aa[3]); -const __m256d bri0 = _mm256_loadu_pd(bb[0]); -const __m256d bri1 = _mm256_loadu_pd(bb[1]); -const __m256d bri2 = _mm256_loadu_pd(bb[2]); -const __m256d bri3 = _mm256_loadu_pd(bb[3]); -const __m256d bir0 = _mm256_shuffle_pd(bri0,bri0, 5); // conj of b -const __m256d bir1 = _mm256_shuffle_pd(bri1,bri1, 5); // conj of b -const __m256d bir2 = _mm256_shuffle_pd(bri2,bri2, 5); // conj of b -const __m256d bir3 = _mm256_shuffle_pd(bri3,bri3, 5); // conj of b -const __m256d aii0 = _mm256_shuffle_pd(ari0,ari0, 15); // im of a -const __m256d aii1 = _mm256_shuffle_pd(ari1,ari1, 15); // im of a -const __m256d aii2 = _mm256_shuffle_pd(ari2,ari2, 15); // im of a -const __m256d aii3 = _mm256_shuffle_pd(ari3,ari3, 15); // im of a -const __m256d pro0 = _mm256_mul_pd(aii0,bir0); -const __m256d pro1 = _mm256_mul_pd(aii1,bir1); -const __m256d pro2 = _mm256_mul_pd(aii2,bir2); -const __m256d pro3 = _mm256_mul_pd(aii3,bir3); -const __m256d arr0 = _mm256_shuffle_pd(ari0,ari0, 0); // rr of a -const __m256d arr1 = _mm256_shuffle_pd(ari1,ari1, 0); // rr of a -const __m256d arr2 = _mm256_shuffle_pd(ari2,ari2, 0); // rr of a -const __m256d arr3 = _mm256_shuffle_pd(ari3,ari3, 0); // rr of a -const __m256d res0 = _mm256_fmaddsub_pd(arr0,bri0,pro0); -const __m256d res1 = _mm256_fmaddsub_pd(arr1,bri1,pro1); -const __m256d res2 = _mm256_fmaddsub_pd(arr2,bri2,pro2); -const __m256d res3 = _mm256_fmaddsub_pd(arr3,bri3,pro3); -_mm256_storeu_pd(rr[0],res0); -_mm256_storeu_pd(rr[1],res1); -_mm256_storeu_pd(rr[2],res2); -_mm256_storeu_pd(rr[3],res3); - // END_INTERLEAVE - rr += 4; - aa += 4; - bb += 4; - } while (aa < aend); -} - -EXPORT void cplx_fftvec_add_fma(uint32_t m, void* r, const void* a, const void* b) { - const double(*aa)[4] = (double(*)[4])a; - const double(*bb)[4] = (double(*)[4])b; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d res% = _mm256_add_pd(ari%,bri%); -_mm256_storeu_pd(rr[%],res%); -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 -const __m256d ari0 = _mm256_loadu_pd(aa[0]); -const __m256d ari1 = _mm256_loadu_pd(aa[1]); -const __m256d ari2 = _mm256_loadu_pd(aa[2]); -const __m256d ari3 = _mm256_loadu_pd(aa[3]); -const __m256d bri0 = _mm256_loadu_pd(bb[0]); -const __m256d bri1 = _mm256_loadu_pd(bb[1]); -const __m256d bri2 = _mm256_loadu_pd(bb[2]); -const __m256d bri3 = _mm256_loadu_pd(bb[3]); -const __m256d res0 = _mm256_add_pd(ari0,bri0); -const __m256d res1 = _mm256_add_pd(ari1,bri1); -const __m256d res2 = _mm256_add_pd(ari2,bri2); -const __m256d res3 = _mm256_add_pd(ari3,bri3); -_mm256_storeu_pd(rr[0],res0); -_mm256_storeu_pd(rr[1],res1); -_mm256_storeu_pd(rr[2],res2); -_mm256_storeu_pd(rr[3],res3); - // END_INTERLEAVE - rr += 4; - aa += 4; - bb += 4; - } while (aa < aend); -} - -EXPORT void cplx_fftvec_sub2_to_fma(uint32_t m, void* r, const void* a, const void* b) { - const double(*aa)[4] = (double(*)[4])a; - const double(*bb)[4] = (double(*)[4])b; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d sum% = _mm256_add_pd(ari%,bri%); -const __m256d rri% = _mm256_loadu_pd(rr[%]); -const __m256d res% = _mm256_sub_pd(rri%,sum%); -_mm256_storeu_pd(rr[%],res%); -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 -const __m256d ari0 = _mm256_loadu_pd(aa[0]); -const __m256d ari1 = _mm256_loadu_pd(aa[1]); -const __m256d ari2 = _mm256_loadu_pd(aa[2]); -const __m256d ari3 = _mm256_loadu_pd(aa[3]); -const __m256d bri0 = _mm256_loadu_pd(bb[0]); -const __m256d bri1 = _mm256_loadu_pd(bb[1]); -const __m256d bri2 = _mm256_loadu_pd(bb[2]); -const __m256d bri3 = _mm256_loadu_pd(bb[3]); -const __m256d sum0 = _mm256_add_pd(ari0,bri0); -const __m256d sum1 = _mm256_add_pd(ari1,bri1); -const __m256d sum2 = _mm256_add_pd(ari2,bri2); -const __m256d sum3 = _mm256_add_pd(ari3,bri3); -const __m256d rri0 = _mm256_loadu_pd(rr[0]); -const __m256d rri1 = _mm256_loadu_pd(rr[1]); -const __m256d rri2 = _mm256_loadu_pd(rr[2]); -const __m256d rri3 = _mm256_loadu_pd(rr[3]); -const __m256d res0 = _mm256_sub_pd(rri0,sum0); -const __m256d res1 = _mm256_sub_pd(rri1,sum1); -const __m256d res2 = _mm256_sub_pd(rri2,sum2); -const __m256d res3 = _mm256_sub_pd(rri3,sum3); -_mm256_storeu_pd(rr[0],res0); -_mm256_storeu_pd(rr[1],res1); -_mm256_storeu_pd(rr[2],res2); -_mm256_storeu_pd(rr[3],res3); - // END_INTERLEAVE - rr += 4; - aa += 4; - bb += 4; - } while (aa < aend); -} - -EXPORT void cplx_fftvec_copy_fma(uint32_t m, void* r, const void* a) { - const double(*aa)[4] = (double(*)[4])a; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -_mm256_storeu_pd(rr[%],ari%); -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 -const __m256d ari0 = _mm256_loadu_pd(aa[0]); -const __m256d ari1 = _mm256_loadu_pd(aa[1]); -const __m256d ari2 = _mm256_loadu_pd(aa[2]); -const __m256d ari3 = _mm256_loadu_pd(aa[3]); -_mm256_storeu_pd(rr[0],ari0); -_mm256_storeu_pd(rr[1],ari1); -_mm256_storeu_pd(rr[2],ari2); -_mm256_storeu_pd(rr[3],ari3); - // END_INTERLEAVE - rr += 4; - aa += 4; - } while (aa < aend); -} - -EXPORT void cplx_fftvec_twiddle_fma(uint32_t m, void* a, void* b, const void* omg) { - double(*aa)[4] = (double(*)[4])a; - double(*bb)[4] = (double(*)[4])b; - const double(*const aend)[4] = aa + (m >> 1); - const __m256d om = _mm256_loadu_pd(omg); - const __m256d omrr = _mm256_shuffle_pd(om, om, 0); - const __m256d omii = _mm256_shuffle_pd(om, om, 15); - do { - /* -BEGIN_TEMPLATE -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d bir% = _mm256_shuffle_pd(bri%,bri%,5); -__m256d p% = _mm256_mul_pd(bir%,omii); -p% = _mm256_fmaddsub_pd(bri%,omrr,p%); -const __m256d ari% = _mm256_loadu_pd(aa[%]); -_mm256_storeu_pd(aa[%],_mm256_add_pd(ari%,p%)); -_mm256_storeu_pd(bb[%],_mm256_sub_pd(ari%,p%)); -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 -const __m256d bri0 = _mm256_loadu_pd(bb[0]); -const __m256d bri1 = _mm256_loadu_pd(bb[1]); -const __m256d bri2 = _mm256_loadu_pd(bb[2]); -const __m256d bri3 = _mm256_loadu_pd(bb[3]); -const __m256d bir0 = _mm256_shuffle_pd(bri0,bri0,5); -const __m256d bir1 = _mm256_shuffle_pd(bri1,bri1,5); -const __m256d bir2 = _mm256_shuffle_pd(bri2,bri2,5); -const __m256d bir3 = _mm256_shuffle_pd(bri3,bri3,5); -__m256d p0 = _mm256_mul_pd(bir0,omii); -__m256d p1 = _mm256_mul_pd(bir1,omii); -__m256d p2 = _mm256_mul_pd(bir2,omii); -__m256d p3 = _mm256_mul_pd(bir3,omii); -p0 = _mm256_fmaddsub_pd(bri0,omrr,p0); -p1 = _mm256_fmaddsub_pd(bri1,omrr,p1); -p2 = _mm256_fmaddsub_pd(bri2,omrr,p2); -p3 = _mm256_fmaddsub_pd(bri3,omrr,p3); -const __m256d ari0 = _mm256_loadu_pd(aa[0]); -const __m256d ari1 = _mm256_loadu_pd(aa[1]); -const __m256d ari2 = _mm256_loadu_pd(aa[2]); -const __m256d ari3 = _mm256_loadu_pd(aa[3]); -_mm256_storeu_pd(aa[0],_mm256_add_pd(ari0,p0)); -_mm256_storeu_pd(aa[1],_mm256_add_pd(ari1,p1)); -_mm256_storeu_pd(aa[2],_mm256_add_pd(ari2,p2)); -_mm256_storeu_pd(aa[3],_mm256_add_pd(ari3,p3)); -_mm256_storeu_pd(bb[0],_mm256_sub_pd(ari0,p0)); -_mm256_storeu_pd(bb[1],_mm256_sub_pd(ari1,p1)); -_mm256_storeu_pd(bb[2],_mm256_sub_pd(ari2,p2)); -_mm256_storeu_pd(bb[3],_mm256_sub_pd(ari3,p3)); - // END_INTERLEAVE - bb += 4; - aa += 4; - } while (aa < aend); -} - -EXPORT void cplx_fftvec_innerprod_avx2_fma(const CPLX_FFTVEC_INNERPROD_PRECOMP* precomp, const int32_t ellbar, - const uint64_t lda, const uint64_t ldb, - void* r, const void* a, const void* b) { - const uint32_t m = precomp->m; - const uint32_t blk = precomp->blk; - const uint32_t nblocks = precomp->nblocks; - const CPLX* aa = (CPLX*)a; - const CPLX* bb = (CPLX*)b; - CPLX* rr = (CPLX*)r; - const uint64_t ldda = lda >> 4; // in CPLX - const uint64_t lddb = ldb >> 4; - if (m==0) { - memset(r, 0, m*sizeof(CPLX)); - return; - } - for (uint32_t k=0; k<nblocks; ++k) { - const uint64_t offset = k*blk; - const CPLX* aaa = aa+offset; - const CPLX* bbb = bb+offset; - CPLX *rrr = rr+offset; - cplx_fftvec_mul_fma(&precomp->mul_func, rrr, aaa, bbb); - for (int32_t i=1; i<ellbar; ++i) { - cplx_fftvec_addmul_fma(&precomp->addmul_func, rrr, aaa + i * ldda, bbb + i * lddb); - } - } -} -#endif -
D spqlios/lib/spqlios/cplx/cplx_fftvec_avx2_fma.c

@@ -1,389 +0,0 @@

-#include <immintrin.h> -#include <string.h> - -#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -typedef double D4MEM[4]; - -EXPORT void cplx_fftvec_mul_fma(const CPLX_FFTVEC_MUL_PRECOMP* precomp, void* r, const void* a, const void* b) { - const uint32_t m = precomp->m; - const double(*aa)[4] = (double(*)[4])a; - const double(*bb)[4] = (double(*)[4])b; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d bir% = _mm256_shuffle_pd(bri%,bri%, 5); // conj of b -const __m256d aii% = _mm256_shuffle_pd(ari%,ari%, 15); // im of a -const __m256d pro% = _mm256_mul_pd(aii%,bir%); -const __m256d arr% = _mm256_shuffle_pd(ari%,ari%, 0); // rr of a -const __m256d res% = _mm256_fmaddsub_pd(arr%,bri%,pro%); -_mm256_storeu_pd(rr[%],res%); -rr += @; // ONCE -aa += @; // ONCE -bb += @; // ONCE -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 - // This block is automatically generated from the template above - // by the interleave.pl script. Please do not edit by hand - const __m256d ari0 = _mm256_loadu_pd(aa[0]); - const __m256d ari1 = _mm256_loadu_pd(aa[1]); - const __m256d ari2 = _mm256_loadu_pd(aa[2]); - const __m256d ari3 = _mm256_loadu_pd(aa[3]); - const __m256d bri0 = _mm256_loadu_pd(bb[0]); - const __m256d bri1 = _mm256_loadu_pd(bb[1]); - const __m256d bri2 = _mm256_loadu_pd(bb[2]); - const __m256d bri3 = _mm256_loadu_pd(bb[3]); - const __m256d bir0 = _mm256_shuffle_pd(bri0, bri0, 5); // conj of b - const __m256d bir1 = _mm256_shuffle_pd(bri1, bri1, 5); // conj of b - const __m256d bir2 = _mm256_shuffle_pd(bri2, bri2, 5); // conj of b - const __m256d bir3 = _mm256_shuffle_pd(bri3, bri3, 5); // conj of b - const __m256d aii0 = _mm256_shuffle_pd(ari0, ari0, 15); // im of a - const __m256d aii1 = _mm256_shuffle_pd(ari1, ari1, 15); // im of a - const __m256d aii2 = _mm256_shuffle_pd(ari2, ari2, 15); // im of a - const __m256d aii3 = _mm256_shuffle_pd(ari3, ari3, 15); // im of a - const __m256d pro0 = _mm256_mul_pd(aii0, bir0); - const __m256d pro1 = _mm256_mul_pd(aii1, bir1); - const __m256d pro2 = _mm256_mul_pd(aii2, bir2); - const __m256d pro3 = _mm256_mul_pd(aii3, bir3); - const __m256d arr0 = _mm256_shuffle_pd(ari0, ari0, 0); // rr of a - const __m256d arr1 = _mm256_shuffle_pd(ari1, ari1, 0); // rr of a - const __m256d arr2 = _mm256_shuffle_pd(ari2, ari2, 0); // rr of a - const __m256d arr3 = _mm256_shuffle_pd(ari3, ari3, 0); // rr of a - const __m256d res0 = _mm256_fmaddsub_pd(arr0, bri0, pro0); - const __m256d res1 = _mm256_fmaddsub_pd(arr1, bri1, pro1); - const __m256d res2 = _mm256_fmaddsub_pd(arr2, bri2, pro2); - const __m256d res3 = _mm256_fmaddsub_pd(arr3, bri3, pro3); - _mm256_storeu_pd(rr[0], res0); - _mm256_storeu_pd(rr[1], res1); - _mm256_storeu_pd(rr[2], res2); - _mm256_storeu_pd(rr[3], res3); - rr += 4; // ONCE - aa += 4; // ONCE - bb += 4; // ONCE - // END_INTERLEAVE - } while (aa < aend); -} - - - -EXPORT void cplx_fftvec_addmul_fma(const CPLX_FFTVEC_ADDMUL_PRECOMP* precomp, void* r, const void* a, const void* b) { - const uint32_t m = precomp->m; - const double(*aa)[4] = (double(*)[4])a; - const double(*bb)[4] = (double(*)[4])b; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d rri% = _mm256_loadu_pd(rr[%]); -const __m256d bir% = _mm256_shuffle_pd(bri%,bri%, 5); -const __m256d aii% = _mm256_shuffle_pd(ari%,ari%, 15); -const __m256d pro% = _mm256_fmaddsub_pd(aii%,bir%,rri%); -const __m256d arr% = _mm256_shuffle_pd(ari%,ari%, 0); -const __m256d res% = _mm256_fmaddsub_pd(arr%,bri%,pro%); -_mm256_storeu_pd(rr[%],res%); -rr += @; // ONCE -aa += @; // ONCE -bb += @; // ONCE -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 2 - // This block is automatically generated from the template above - // by the interleave.pl script. Please do not edit by hand - const __m256d ari0 = _mm256_loadu_pd(aa[0]); - const __m256d ari1 = _mm256_loadu_pd(aa[1]); - const __m256d bri0 = _mm256_loadu_pd(bb[0]); - const __m256d bri1 = _mm256_loadu_pd(bb[1]); - const __m256d rri0 = _mm256_loadu_pd(rr[0]); - const __m256d rri1 = _mm256_loadu_pd(rr[1]); - const __m256d bir0 = _mm256_shuffle_pd(bri0, bri0, 5); - const __m256d bir1 = _mm256_shuffle_pd(bri1, bri1, 5); - const __m256d aii0 = _mm256_shuffle_pd(ari0, ari0, 15); - const __m256d aii1 = _mm256_shuffle_pd(ari1, ari1, 15); - const __m256d pro0 = _mm256_fmaddsub_pd(aii0, bir0, rri0); - const __m256d pro1 = _mm256_fmaddsub_pd(aii1, bir1, rri1); - const __m256d arr0 = _mm256_shuffle_pd(ari0, ari0, 0); - const __m256d arr1 = _mm256_shuffle_pd(ari1, ari1, 0); - const __m256d res0 = _mm256_fmaddsub_pd(arr0, bri0, pro0); - const __m256d res1 = _mm256_fmaddsub_pd(arr1, bri1, pro1); - _mm256_storeu_pd(rr[0], res0); - _mm256_storeu_pd(rr[1], res1); - rr += 2; // ONCE - aa += 2; // ONCE - bb += 2; // ONCE - // END_INTERLEAVE - } while (aa < aend); -} - -EXPORT void cplx_fftvec_bitwiddle_fma(const CPLX_FFTVEC_BITWIDDLE_PRECOMP* precomp, void* a, uint64_t slicea, - const void* omg) { - const uint32_t m = precomp->m; - const uint64_t OFFSET = slicea / sizeof(D4MEM); - D4MEM* aa = (D4MEM*)a; - const double(*const aend)[4] = aa + (m >> 1); - const __m256d om = _mm256_loadu_pd(omg); - const __m256d om1rr = _mm256_shuffle_pd(om, om, 0); - const __m256d om1ii = _mm256_shuffle_pd(om, om, 15); - const __m256d om2rr = _mm256_shuffle_pd(om, om, 0); - const __m256d om2ii = _mm256_shuffle_pd(om, om, 0); - const __m256d om3rr = _mm256_shuffle_pd(om, om, 15); - const __m256d om3ii = _mm256_shuffle_pd(om, om, 15); - do { - /* -BEGIN_TEMPLATE -__m256d ari% = _mm256_loadu_pd(aa[%]); -__m256d bri% = _mm256_loadu_pd((aa+OFFSET)[%]); -__m256d cri% = _mm256_loadu_pd((aa+2*OFFSET)[%]); -__m256d dri% = _mm256_loadu_pd((aa+3*OFFSET)[%]); -__m256d pa% = _mm256_shuffle_pd(cri%,cri%,5); -__m256d pb% = _mm256_shuffle_pd(dri%,dri%,5); -pa% = _mm256_mul_pd(pa%,om1ii); -pb% = _mm256_mul_pd(pb%,om1ii); -pa% = _mm256_fmaddsub_pd(cri%,om1rr,pa%); -pb% = _mm256_fmaddsub_pd(dri%,om1rr,pb%); -cri% = _mm256_sub_pd(ari%,pa%); -dri% = _mm256_sub_pd(bri%,pb%); -ari% = _mm256_add_pd(ari%,pa%); -bri% = _mm256_add_pd(bri%,pb%); -pa% = _mm256_shuffle_pd(bri%,bri%,5); -pb% = _mm256_shuffle_pd(dri%,dri%,5); -pa% = _mm256_mul_pd(pa%,om2ii); -pb% = _mm256_mul_pd(pb%,om3ii); -pa% = _mm256_fmaddsub_pd(bri%,om2rr,pa%); -pb% = _mm256_fmaddsub_pd(dri%,om3rr,pb%); -bri% = _mm256_sub_pd(ari%,pa%); -dri% = _mm256_sub_pd(cri%,pb%); -ari% = _mm256_add_pd(ari%,pa%); -cri% = _mm256_add_pd(cri%,pb%); -_mm256_storeu_pd(aa[%], ari%); -_mm256_storeu_pd((aa+OFFSET)[%],bri%); -_mm256_storeu_pd((aa+2*OFFSET)[%],cri%); -_mm256_storeu_pd((aa+3*OFFSET)[%],dri%); -aa += @; // ONCE -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 1 - // This block is automatically generated from the template above - // by the interleave.pl script. Please do not edit by hand - __m256d ari0 = _mm256_loadu_pd(aa[0]); - __m256d bri0 = _mm256_loadu_pd((aa + OFFSET)[0]); - __m256d cri0 = _mm256_loadu_pd((aa + 2 * OFFSET)[0]); - __m256d dri0 = _mm256_loadu_pd((aa + 3 * OFFSET)[0]); - __m256d pa0 = _mm256_shuffle_pd(cri0, cri0, 5); - __m256d pb0 = _mm256_shuffle_pd(dri0, dri0, 5); - pa0 = _mm256_mul_pd(pa0, om1ii); - pb0 = _mm256_mul_pd(pb0, om1ii); - pa0 = _mm256_fmaddsub_pd(cri0, om1rr, pa0); - pb0 = _mm256_fmaddsub_pd(dri0, om1rr, pb0); - cri0 = _mm256_sub_pd(ari0, pa0); - dri0 = _mm256_sub_pd(bri0, pb0); - ari0 = _mm256_add_pd(ari0, pa0); - bri0 = _mm256_add_pd(bri0, pb0); - pa0 = _mm256_shuffle_pd(bri0, bri0, 5); - pb0 = _mm256_shuffle_pd(dri0, dri0, 5); - pa0 = _mm256_mul_pd(pa0, om2ii); - pb0 = _mm256_mul_pd(pb0, om3ii); - pa0 = _mm256_fmaddsub_pd(bri0, om2rr, pa0); - pb0 = _mm256_fmaddsub_pd(dri0, om3rr, pb0); - bri0 = _mm256_sub_pd(ari0, pa0); - dri0 = _mm256_sub_pd(cri0, pb0); - ari0 = _mm256_add_pd(ari0, pa0); - cri0 = _mm256_add_pd(cri0, pb0); - _mm256_storeu_pd(aa[0], ari0); - _mm256_storeu_pd((aa + OFFSET)[0], bri0); - _mm256_storeu_pd((aa + 2 * OFFSET)[0], cri0); - _mm256_storeu_pd((aa + 3 * OFFSET)[0], dri0); - aa += 1; // ONCE - // END_INTERLEAVE - } while (aa < aend); -} - -EXPORT void cplx_fftvec_sub2_to_fma(uint32_t m, void* r, const void* a, const void* b) { - const double(*aa)[4] = (double(*)[4])a; - const double(*bb)[4] = (double(*)[4])b; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d sum% = _mm256_add_pd(ari%,bri%); -const __m256d rri% = _mm256_loadu_pd(rr[%]); -const __m256d res% = _mm256_sub_pd(rri%,sum%); -_mm256_storeu_pd(rr[%],res%); -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 - // This block is automatically generated from the template above - // by the interleave.pl script. Please do not edit by hand - const __m256d ari0 = _mm256_loadu_pd(aa[0]); - const __m256d ari1 = _mm256_loadu_pd(aa[1]); - const __m256d ari2 = _mm256_loadu_pd(aa[2]); - const __m256d ari3 = _mm256_loadu_pd(aa[3]); - const __m256d bri0 = _mm256_loadu_pd(bb[0]); - const __m256d bri1 = _mm256_loadu_pd(bb[1]); - const __m256d bri2 = _mm256_loadu_pd(bb[2]); - const __m256d bri3 = _mm256_loadu_pd(bb[3]); - const __m256d sum0 = _mm256_add_pd(ari0, bri0); - const __m256d sum1 = _mm256_add_pd(ari1, bri1); - const __m256d sum2 = _mm256_add_pd(ari2, bri2); - const __m256d sum3 = _mm256_add_pd(ari3, bri3); - const __m256d rri0 = _mm256_loadu_pd(rr[0]); - const __m256d rri1 = _mm256_loadu_pd(rr[1]); - const __m256d rri2 = _mm256_loadu_pd(rr[2]); - const __m256d rri3 = _mm256_loadu_pd(rr[3]); - const __m256d res0 = _mm256_sub_pd(rri0, sum0); - const __m256d res1 = _mm256_sub_pd(rri1, sum1); - const __m256d res2 = _mm256_sub_pd(rri2, sum2); - const __m256d res3 = _mm256_sub_pd(rri3, sum3); - _mm256_storeu_pd(rr[0], res0); - _mm256_storeu_pd(rr[1], res1); - _mm256_storeu_pd(rr[2], res2); - _mm256_storeu_pd(rr[3], res3); - // END_INTERLEAVE - rr += 4; - aa += 4; - bb += 4; - } while (aa < aend); -} - -EXPORT void cplx_fftvec_add_fma(uint32_t m, void* r, const void* a, const void* b) { - const double(*aa)[4] = (double(*)[4])a; - const double(*bb)[4] = (double(*)[4])b; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* - BEGIN_TEMPLATE - const __m256d ari% = _mm256_loadu_pd(aa[%]); - const __m256d bri% = _mm256_loadu_pd(bb[%]); - const __m256d res% = _mm256_add_pd(ari%,bri%); - _mm256_storeu_pd(rr[%],res%); - rr += @; // ONCE - aa += @; // ONCE - bb += @; // ONCE - END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 - // This block is automatically generated from the template above - // by the interleave.pl script. Please do not edit by hand - const __m256d ari0 = _mm256_loadu_pd(aa[0]); - const __m256d ari1 = _mm256_loadu_pd(aa[1]); - const __m256d ari2 = _mm256_loadu_pd(aa[2]); - const __m256d ari3 = _mm256_loadu_pd(aa[3]); - const __m256d bri0 = _mm256_loadu_pd(bb[0]); - const __m256d bri1 = _mm256_loadu_pd(bb[1]); - const __m256d bri2 = _mm256_loadu_pd(bb[2]); - const __m256d bri3 = _mm256_loadu_pd(bb[3]); - const __m256d res0 = _mm256_add_pd(ari0, bri0); - const __m256d res1 = _mm256_add_pd(ari1, bri1); - const __m256d res2 = _mm256_add_pd(ari2, bri2); - const __m256d res3 = _mm256_add_pd(ari3, bri3); - _mm256_storeu_pd(rr[0], res0); - _mm256_storeu_pd(rr[1], res1); - _mm256_storeu_pd(rr[2], res2); - _mm256_storeu_pd(rr[3], res3); - rr += 4; // ONCE - aa += 4; // ONCE - bb += 4; // ONCE - // END_INTERLEAVE - } while (aa < aend); -} - -EXPORT void cplx_fftvec_twiddle_fma(const CPLX_FFTVEC_TWIDDLE_PRECOMP* precomp, void* a, void* b, const void* omg) { - const uint32_t m = precomp->m; - double(*aa)[4] = (double(*)[4])a; - double(*bb)[4] = (double(*)[4])b; - const double(*const aend)[4] = aa + (m >> 1); - const __m256d om = _mm256_loadu_pd(omg); - const __m256d omrr = _mm256_shuffle_pd(om, om, 0); - const __m256d omii = _mm256_shuffle_pd(om, om, 15); - do { - /* -BEGIN_TEMPLATE -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d bir% = _mm256_shuffle_pd(bri%,bri%,5); -__m256d p% = _mm256_mul_pd(bir%,omii); -p% = _mm256_fmaddsub_pd(bri%,omrr,p%); -const __m256d ari% = _mm256_loadu_pd(aa[%]); -_mm256_storeu_pd(aa[%],_mm256_add_pd(ari%,p%)); -_mm256_storeu_pd(bb[%],_mm256_sub_pd(ari%,p%)); -bb += @; // ONCE -aa += @; // ONCE -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 - // This block is automatically generated from the template above - // by the interleave.pl script. Please do not edit by hand - const __m256d bri0 = _mm256_loadu_pd(bb[0]); - const __m256d bri1 = _mm256_loadu_pd(bb[1]); - const __m256d bri2 = _mm256_loadu_pd(bb[2]); - const __m256d bri3 = _mm256_loadu_pd(bb[3]); - const __m256d bir0 = _mm256_shuffle_pd(bri0, bri0, 5); - const __m256d bir1 = _mm256_shuffle_pd(bri1, bri1, 5); - const __m256d bir2 = _mm256_shuffle_pd(bri2, bri2, 5); - const __m256d bir3 = _mm256_shuffle_pd(bri3, bri3, 5); - __m256d p0 = _mm256_mul_pd(bir0, omii); - __m256d p1 = _mm256_mul_pd(bir1, omii); - __m256d p2 = _mm256_mul_pd(bir2, omii); - __m256d p3 = _mm256_mul_pd(bir3, omii); - p0 = _mm256_fmaddsub_pd(bri0, omrr, p0); - p1 = _mm256_fmaddsub_pd(bri1, omrr, p1); - p2 = _mm256_fmaddsub_pd(bri2, omrr, p2); - p3 = _mm256_fmaddsub_pd(bri3, omrr, p3); - const __m256d ari0 = _mm256_loadu_pd(aa[0]); - const __m256d ari1 = _mm256_loadu_pd(aa[1]); - const __m256d ari2 = _mm256_loadu_pd(aa[2]); - const __m256d ari3 = _mm256_loadu_pd(aa[3]); - _mm256_storeu_pd(aa[0], _mm256_add_pd(ari0, p0)); - _mm256_storeu_pd(aa[1], _mm256_add_pd(ari1, p1)); - _mm256_storeu_pd(aa[2], _mm256_add_pd(ari2, p2)); - _mm256_storeu_pd(aa[3], _mm256_add_pd(ari3, p3)); - _mm256_storeu_pd(bb[0], _mm256_sub_pd(ari0, p0)); - _mm256_storeu_pd(bb[1], _mm256_sub_pd(ari1, p1)); - _mm256_storeu_pd(bb[2], _mm256_sub_pd(ari2, p2)); - _mm256_storeu_pd(bb[3], _mm256_sub_pd(ari3, p3)); - bb += 4; // ONCE - aa += 4; // ONCE - // END_INTERLEAVE - } while (aa < aend); -} - -EXPORT void cplx_fftvec_copy_fma(uint32_t m, void* r, const void* a) { - const double(*aa)[4] = (double(*)[4])a; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -_mm256_storeu_pd(rr[%],ari%); -rr += @; // ONCE -aa += @; // ONCE -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 - // This block is automatically generated from the template above - // by the interleave.pl script. Please do not edit by hand - const __m256d ari0 = _mm256_loadu_pd(aa[0]); - const __m256d ari1 = _mm256_loadu_pd(aa[1]); - const __m256d ari2 = _mm256_loadu_pd(aa[2]); - const __m256d ari3 = _mm256_loadu_pd(aa[3]); - _mm256_storeu_pd(rr[0], ari0); - _mm256_storeu_pd(rr[1], ari1); - _mm256_storeu_pd(rr[2], ari2); - _mm256_storeu_pd(rr[3], ari3); - rr += 4; // ONCE - aa += 4; // ONCE - // END_INTERLEAVE - } while (aa < aend); -}
D spqlios/lib/spqlios/cplx/cplx_fftvec_ref.c

@@ -1,85 +0,0 @@

-#include <string.h> - -#include "../commons_private.h" -#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -EXPORT void cplx_fftvec_addmul_ref(const CPLX_FFTVEC_ADDMUL_PRECOMP* precomp, void* r, const void* a, const void* b) { - const uint32_t m = precomp->m; - const CPLX* aa = (CPLX*)a; - const CPLX* bb = (CPLX*)b; - CPLX* rr = (CPLX*)r; - for (uint32_t i = 0; i < m; ++i) { - const double re = aa[i][0] * bb[i][0] - aa[i][1] * bb[i][1]; - const double im = aa[i][0] * bb[i][1] + aa[i][1] * bb[i][0]; - rr[i][0] += re; - rr[i][1] += im; - } -} - -EXPORT void cplx_fftvec_mul_ref(const CPLX_FFTVEC_MUL_PRECOMP* precomp, void* r, const void* a, const void* b) { - const uint32_t m = precomp->m; - const CPLX* aa = (CPLX*)a; - const CPLX* bb = (CPLX*)b; - CPLX* rr = (CPLX*)r; - for (uint32_t i = 0; i < m; ++i) { - const double re = aa[i][0] * bb[i][0] - aa[i][1] * bb[i][1]; - const double im = aa[i][0] * bb[i][1] + aa[i][1] * bb[i][0]; - rr[i][0] = re; - rr[i][1] = im; - } -} - -EXPORT void* init_cplx_fftvec_addmul_precomp(CPLX_FFTVEC_ADDMUL_PRECOMP* r, uint32_t m) { - if (m & (m - 1)) return spqlios_error("m must be a power of two"); - r->m = m; - if (m <= 4) { - r->function = cplx_fftvec_addmul_ref; - } else if (CPU_SUPPORTS("fma")) { - r->function = cplx_fftvec_addmul_fma; - } else { - r->function = cplx_fftvec_addmul_ref; - } - return r; -} - -EXPORT void* init_cplx_fftvec_mul_precomp(CPLX_FFTVEC_MUL_PRECOMP* r, uint32_t m) { - if (m & (m - 1)) return spqlios_error("m must be a power of two"); - r->m = m; - if (m <= 4) { - r->function = cplx_fftvec_mul_ref; - } else if (CPU_SUPPORTS("fma")) { - r->function = cplx_fftvec_mul_fma; - } else { - r->function = cplx_fftvec_mul_ref; - } - return r; -} - -EXPORT CPLX_FFTVEC_ADDMUL_PRECOMP* new_cplx_fftvec_addmul_precomp(uint32_t m) { - CPLX_FFTVEC_ADDMUL_PRECOMP* r = malloc(sizeof(CPLX_FFTVEC_MUL_PRECOMP)); - return spqlios_keep_or_free(r, init_cplx_fftvec_addmul_precomp(r, m)); -} - -EXPORT CPLX_FFTVEC_MUL_PRECOMP* new_cplx_fftvec_mul_precomp(uint32_t m) { - CPLX_FFTVEC_MUL_PRECOMP* r = malloc(sizeof(CPLX_FFTVEC_MUL_PRECOMP)); - return spqlios_keep_or_free(r, init_cplx_fftvec_mul_precomp(r, m)); -} - -EXPORT void cplx_fftvec_mul_simple(uint32_t m, void* r, const void* a, const void* b) { - static CPLX_FFTVEC_MUL_PRECOMP p[31] = {0}; - CPLX_FFTVEC_MUL_PRECOMP* f = p + log2m(m); - if (!f->function) { - if (!init_cplx_fftvec_mul_precomp(f, m)) abort(); - } - f->function(f, r, a, b); -} - -EXPORT void cplx_fftvec_addmul_simple(uint32_t m, void* r, const void* a, const void* b) { - static CPLX_FFTVEC_ADDMUL_PRECOMP p[31] = {0}; - CPLX_FFTVEC_ADDMUL_PRECOMP* f = p + log2m(m); - if (!f->function) { - if (!init_cplx_fftvec_addmul_precomp(f, m)) abort(); - } - f->function(f, r, a, b); -}
D spqlios/lib/spqlios/cplx/cplx_ifft16_avx_fma.s

@@ -1,157 +0,0 @@

-# shifted FFT over X^16-i -# 1st argument (rdi) contains 16 complexes -# 2nd argument (rsi) contains: 8 complexes -# omega,alpha,beta,j.beta,gamma,j.gamma,k.gamma,kj.gamma -# alpha = sqrt(omega), beta = sqrt(alpha), gamma = sqrt(beta) -# j = sqrt(i), k=sqrt(j) -.globl cplx_ifft16_avx_fma -cplx_ifft16_avx_fma: -vmovupd (%rdi),%ymm8 # load data into registers %ymm8 -> %ymm15 -vmovupd 0x20(%rdi),%ymm9 -vmovupd 0x40(%rdi),%ymm10 -vmovupd 0x60(%rdi),%ymm11 -vmovupd 0x80(%rdi),%ymm12 -vmovupd 0xa0(%rdi),%ymm13 -vmovupd 0xc0(%rdi),%ymm14 -vmovupd 0xe0(%rdi),%ymm15 - -.fourth_pass: -vmovupd 0(%rsi),%ymm0 /* gamma */ -vmovupd 32(%rsi),%ymm2 /* delta */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: gama.iiii */ -vshufpd $15, %ymm2, %ymm2, %ymm3 /* ymm3: delta.iiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: gama.rrrr */ -vshufpd $0, %ymm2, %ymm2, %ymm2 /* ymm2: delta.rrrr */ -vperm2f128 $0x31,%ymm10,%ymm8,%ymm4 # ymm4 contains c1,c5 -vperm2f128 $0x31,%ymm11,%ymm9,%ymm5 # ymm5 contains c3,c7 -vperm2f128 $0x31,%ymm14,%ymm12,%ymm6 # ymm6 contains c9,c13 -vperm2f128 $0x31,%ymm15,%ymm13,%ymm7 # ymm7 contains c11,c15 -vperm2f128 $0x20,%ymm10,%ymm8,%ymm8 # ymm8 contains c0,c4 -vperm2f128 $0x20,%ymm11,%ymm9,%ymm9 # ymm9 contains c2,c6 -vperm2f128 $0x20,%ymm14,%ymm12,%ymm10 # ymm10 contains c8,c12 -vperm2f128 $0x20,%ymm15,%ymm13,%ymm11 # ymm11 contains c10,c14 -vsubpd %ymm4,%ymm8,%ymm12 # tw: to mul by gamma -vsubpd %ymm5,%ymm9,%ymm13 # itw: to mul by i.gamma -vsubpd %ymm6,%ymm10,%ymm14 # tw: to mul by delta -vsubpd %ymm7,%ymm11,%ymm15 # itw: to mul by i.delta -vaddpd %ymm4,%ymm8,%ymm8 -vaddpd %ymm5,%ymm9,%ymm9 -vaddpd %ymm6,%ymm10,%ymm10 -vaddpd %ymm7,%ymm11,%ymm11 -vshufpd $5, %ymm12, %ymm12, %ymm4 -vshufpd $5, %ymm13, %ymm13, %ymm5 -vshufpd $5, %ymm14, %ymm14, %ymm6 -vshufpd $5, %ymm15, %ymm15, %ymm7 -vmulpd %ymm4,%ymm1,%ymm4 -vmulpd %ymm5,%ymm0,%ymm5 -vmulpd %ymm6,%ymm3,%ymm6 -vmulpd %ymm7,%ymm2,%ymm7 -vfmaddsub231pd %ymm12, %ymm0, %ymm4 # ymm4 = (ymm0 * ymm12) +/- ymm4 -vfmsubadd231pd %ymm13, %ymm1, %ymm5 -vfmaddsub231pd %ymm14, %ymm2, %ymm6 -vfmsubadd231pd %ymm15, %ymm3, %ymm7 - -vperm2f128 $0x20,%ymm6,%ymm10,%ymm12 # ymm4 contains c1,c5 -- x gamma -vperm2f128 $0x20,%ymm7,%ymm11,%ymm13 # ymm5 contains c3,c7 -- x igamma -vperm2f128 $0x31,%ymm6,%ymm10,%ymm14 # ymm6 contains c9,c13 -- x delta -vperm2f128 $0x31,%ymm7,%ymm11,%ymm15 # ymm7 contains c11,c15 -- x idelta -vperm2f128 $0x31,%ymm4,%ymm8,%ymm10 # ymm10 contains c8,c12 -vperm2f128 $0x31,%ymm5,%ymm9,%ymm11 # ymm11 contains c10,c14 -vperm2f128 $0x20,%ymm4,%ymm8,%ymm8 # ymm8 contains c0,c4 -vperm2f128 $0x20,%ymm5,%ymm9,%ymm9 # ymm9 contains c2,c6 - - -.third_pass: -vmovupd 64(%rsi),%xmm0 /* gamma */ -vmovupd 80(%rsi),%xmm2 /* delta */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 -vinsertf128 $1, %xmm2, %ymm2, %ymm2 -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: gama.iiii */ -vshufpd $15, %ymm2, %ymm2, %ymm3 /* ymm3: delta.iiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: gama.rrrr */ -vshufpd $0, %ymm2, %ymm2, %ymm2 /* ymm2: delta.rrrr */ -vsubpd %ymm9,%ymm8,%ymm4 -vsubpd %ymm11,%ymm10,%ymm5 -vsubpd %ymm13,%ymm12,%ymm6 -vsubpd %ymm15,%ymm14,%ymm7 -vaddpd %ymm9,%ymm8,%ymm8 -vaddpd %ymm11,%ymm10,%ymm10 -vaddpd %ymm13,%ymm12,%ymm12 -vaddpd %ymm15,%ymm14,%ymm14 -vshufpd $5, %ymm4, %ymm4, %ymm9 -vshufpd $5, %ymm5, %ymm5, %ymm11 -vshufpd $5, %ymm6, %ymm6, %ymm13 -vshufpd $5, %ymm7, %ymm7, %ymm15 -vmulpd %ymm9,%ymm1,%ymm9 -vmulpd %ymm11,%ymm0,%ymm11 -vmulpd %ymm13,%ymm3,%ymm13 -vmulpd %ymm15,%ymm2,%ymm15 -vfmaddsub231pd %ymm4, %ymm0, %ymm9 # ymm9 = (ymm0 * ymm4) +/- ymm9 -vfmsubadd231pd %ymm5, %ymm1, %ymm11 -vfmaddsub231pd %ymm6, %ymm2, %ymm13 -vfmsubadd231pd %ymm7, %ymm3, %ymm15 - -.second_pass: -vmovupd 96(%rsi),%xmm0 /* omri */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 /* omriri */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: omiiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: omrrrr */ -vsubpd %ymm10,%ymm8,%ymm4 -vsubpd %ymm11,%ymm9,%ymm5 -vsubpd %ymm14,%ymm12,%ymm6 -vsubpd %ymm15,%ymm13,%ymm7 -vaddpd %ymm10,%ymm8,%ymm8 -vaddpd %ymm11,%ymm9,%ymm9 -vaddpd %ymm14,%ymm12,%ymm12 -vaddpd %ymm15,%ymm13,%ymm13 -vshufpd $5, %ymm4, %ymm4, %ymm10 -vshufpd $5, %ymm5, %ymm5, %ymm11 -vshufpd $5, %ymm6, %ymm6, %ymm14 -vshufpd $5, %ymm7, %ymm7, %ymm15 -vmulpd %ymm10,%ymm1,%ymm10 -vmulpd %ymm11,%ymm1,%ymm11 -vmulpd %ymm14,%ymm0,%ymm14 -vmulpd %ymm15,%ymm0,%ymm15 -vfmaddsub231pd %ymm4, %ymm0, %ymm10 # ymm10 = (ymm0 * ymm4) +/- ymm10 -vfmaddsub231pd %ymm5, %ymm0, %ymm11 -vfmsubadd231pd %ymm6, %ymm1, %ymm14 -vfmsubadd231pd %ymm7, %ymm1, %ymm15 - -.first_pass: -vmovupd 112(%rsi),%xmm0 /* omri */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 /* omriri */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: omiiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: omrrrr */ -vsubpd %ymm12,%ymm8,%ymm4 -vsubpd %ymm13,%ymm9,%ymm5 -vsubpd %ymm14,%ymm10,%ymm6 -vsubpd %ymm15,%ymm11,%ymm7 -vaddpd %ymm12,%ymm8,%ymm8 -vaddpd %ymm13,%ymm9,%ymm9 -vaddpd %ymm14,%ymm10,%ymm10 -vaddpd %ymm15,%ymm11,%ymm11 -vshufpd $5, %ymm4, %ymm4, %ymm12 -vshufpd $5, %ymm5, %ymm5, %ymm13 -vshufpd $5, %ymm6, %ymm6, %ymm14 -vshufpd $5, %ymm7, %ymm7, %ymm15 -vmulpd %ymm12,%ymm1,%ymm12 -vmulpd %ymm13,%ymm1,%ymm13 -vmulpd %ymm14,%ymm1,%ymm14 -vmulpd %ymm15,%ymm1,%ymm15 -vfmaddsub231pd %ymm4, %ymm0, %ymm12 # ymm12 = (ymm0 * ymm4) +/- ymm12 -vfmaddsub231pd %ymm5, %ymm0, %ymm13 -vfmaddsub231pd %ymm6, %ymm0, %ymm14 -vfmaddsub231pd %ymm7, %ymm0, %ymm15 - -.save_and_return: -vmovupd %ymm8,(%rdi) -vmovupd %ymm9,0x20(%rdi) -vmovupd %ymm10,0x40(%rdi) -vmovupd %ymm11,0x60(%rdi) -vmovupd %ymm12,0x80(%rdi) -vmovupd %ymm13,0xa0(%rdi) -vmovupd %ymm14,0xc0(%rdi) -vmovupd %ymm15,0xe0(%rdi) -ret -.size cplx_ifft16_avx_fma, .-cplx_ifft16_avx_fma -.section .note.GNU-stack,"",@progbits
D spqlios/lib/spqlios/cplx/cplx_ifft16_avx_fma_win32.s

@@ -1,192 +0,0 @@

- .text - .p2align 4 - .globl cplx_ifft16_avx_fma - .def cplx_ifft16_avx_fma; .scl 2; .type 32; .endef -cplx_ifft16_avx_fma: - - pushq %rdi - pushq %rsi - movq %rcx,%rdi - movq %rdx,%rsi - subq $0x100,%rsp - movdqu %xmm6,(%rsp) - movdqu %xmm7,0x10(%rsp) - movdqu %xmm8,0x20(%rsp) - movdqu %xmm9,0x30(%rsp) - movdqu %xmm10,0x40(%rsp) - movdqu %xmm11,0x50(%rsp) - movdqu %xmm12,0x60(%rsp) - movdqu %xmm13,0x70(%rsp) - movdqu %xmm14,0x80(%rsp) - movdqu %xmm15,0x90(%rsp) - callq cplx_ifft16_avx_fma_amd64 - movdqu (%rsp),%xmm6 - movdqu 0x10(%rsp),%xmm7 - movdqu 0x20(%rsp),%xmm8 - movdqu 0x30(%rsp),%xmm9 - movdqu 0x40(%rsp),%xmm10 - movdqu 0x50(%rsp),%xmm11 - movdqu 0x60(%rsp),%xmm12 - movdqu 0x70(%rsp),%xmm13 - movdqu 0x80(%rsp),%xmm14 - movdqu 0x90(%rsp),%xmm15 - addq $0x100,%rsp - popq %rsi - popq %rdi - retq - -# shifted FFT over X^16-i -# 1st argument (rdi) contains 16 complexes -# 2nd argument (rsi) contains: 8 complexes -# omega,alpha,beta,j.beta,gamma,j.gamma,k.gamma,kj.gamma -# alpha = sqrt(omega), beta = sqrt(alpha), gamma = sqrt(beta) -# j = sqrt(i), k=sqrt(j) - -cplx_ifft16_avx_fma_amd64: -vmovupd (%rdi),%ymm8 # load data into registers %ymm8 -> %ymm15 -vmovupd 0x20(%rdi),%ymm9 -vmovupd 0x40(%rdi),%ymm10 -vmovupd 0x60(%rdi),%ymm11 -vmovupd 0x80(%rdi),%ymm12 -vmovupd 0xa0(%rdi),%ymm13 -vmovupd 0xc0(%rdi),%ymm14 -vmovupd 0xe0(%rdi),%ymm15 - -.fourth_pass: -vmovupd 0(%rsi),%ymm0 /* gamma */ -vmovupd 32(%rsi),%ymm2 /* delta */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: gama.iiii */ -vshufpd $15, %ymm2, %ymm2, %ymm3 /* ymm3: delta.iiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: gama.rrrr */ -vshufpd $0, %ymm2, %ymm2, %ymm2 /* ymm2: delta.rrrr */ -vperm2f128 $0x31,%ymm10,%ymm8,%ymm4 # ymm4 contains c1,c5 -vperm2f128 $0x31,%ymm11,%ymm9,%ymm5 # ymm5 contains c3,c7 -vperm2f128 $0x31,%ymm14,%ymm12,%ymm6 # ymm6 contains c9,c13 -vperm2f128 $0x31,%ymm15,%ymm13,%ymm7 # ymm7 contains c11,c15 -vperm2f128 $0x20,%ymm10,%ymm8,%ymm8 # ymm8 contains c0,c4 -vperm2f128 $0x20,%ymm11,%ymm9,%ymm9 # ymm9 contains c2,c6 -vperm2f128 $0x20,%ymm14,%ymm12,%ymm10 # ymm10 contains c8,c12 -vperm2f128 $0x20,%ymm15,%ymm13,%ymm11 # ymm11 contains c10,c14 -vsubpd %ymm4,%ymm8,%ymm12 # tw: to mul by gamma -vsubpd %ymm5,%ymm9,%ymm13 # itw: to mul by i.gamma -vsubpd %ymm6,%ymm10,%ymm14 # tw: to mul by delta -vsubpd %ymm7,%ymm11,%ymm15 # itw: to mul by i.delta -vaddpd %ymm4,%ymm8,%ymm8 -vaddpd %ymm5,%ymm9,%ymm9 -vaddpd %ymm6,%ymm10,%ymm10 -vaddpd %ymm7,%ymm11,%ymm11 -vshufpd $5, %ymm12, %ymm12, %ymm4 -vshufpd $5, %ymm13, %ymm13, %ymm5 -vshufpd $5, %ymm14, %ymm14, %ymm6 -vshufpd $5, %ymm15, %ymm15, %ymm7 -vmulpd %ymm4,%ymm1,%ymm4 -vmulpd %ymm5,%ymm0,%ymm5 -vmulpd %ymm6,%ymm3,%ymm6 -vmulpd %ymm7,%ymm2,%ymm7 -vfmaddsub231pd %ymm12, %ymm0, %ymm4 # ymm4 = (ymm0 * ymm12) +/- ymm4 -vfmsubadd231pd %ymm13, %ymm1, %ymm5 -vfmaddsub231pd %ymm14, %ymm2, %ymm6 -vfmsubadd231pd %ymm15, %ymm3, %ymm7 - -vperm2f128 $0x20,%ymm6,%ymm10,%ymm12 # ymm4 contains c1,c5 -- x gamma -vperm2f128 $0x20,%ymm7,%ymm11,%ymm13 # ymm5 contains c3,c7 -- x igamma -vperm2f128 $0x31,%ymm6,%ymm10,%ymm14 # ymm6 contains c9,c13 -- x delta -vperm2f128 $0x31,%ymm7,%ymm11,%ymm15 # ymm7 contains c11,c15 -- x idelta -vperm2f128 $0x31,%ymm4,%ymm8,%ymm10 # ymm10 contains c8,c12 -vperm2f128 $0x31,%ymm5,%ymm9,%ymm11 # ymm11 contains c10,c14 -vperm2f128 $0x20,%ymm4,%ymm8,%ymm8 # ymm8 contains c0,c4 -vperm2f128 $0x20,%ymm5,%ymm9,%ymm9 # ymm9 contains c2,c6 - - -.third_pass: -vmovupd 64(%rsi),%xmm0 /* gamma */ -vmovupd 80(%rsi),%xmm2 /* delta */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 -vinsertf128 $1, %xmm2, %ymm2, %ymm2 -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: gama.iiii */ -vshufpd $15, %ymm2, %ymm2, %ymm3 /* ymm3: delta.iiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: gama.rrrr */ -vshufpd $0, %ymm2, %ymm2, %ymm2 /* ymm2: delta.rrrr */ -vsubpd %ymm9,%ymm8,%ymm4 -vsubpd %ymm11,%ymm10,%ymm5 -vsubpd %ymm13,%ymm12,%ymm6 -vsubpd %ymm15,%ymm14,%ymm7 -vaddpd %ymm9,%ymm8,%ymm8 -vaddpd %ymm11,%ymm10,%ymm10 -vaddpd %ymm13,%ymm12,%ymm12 -vaddpd %ymm15,%ymm14,%ymm14 -vshufpd $5, %ymm4, %ymm4, %ymm9 -vshufpd $5, %ymm5, %ymm5, %ymm11 -vshufpd $5, %ymm6, %ymm6, %ymm13 -vshufpd $5, %ymm7, %ymm7, %ymm15 -vmulpd %ymm9,%ymm1,%ymm9 -vmulpd %ymm11,%ymm0,%ymm11 -vmulpd %ymm13,%ymm3,%ymm13 -vmulpd %ymm15,%ymm2,%ymm15 -vfmaddsub231pd %ymm4, %ymm0, %ymm9 # ymm9 = (ymm0 * ymm4) +/- ymm9 -vfmsubadd231pd %ymm5, %ymm1, %ymm11 -vfmaddsub231pd %ymm6, %ymm2, %ymm13 -vfmsubadd231pd %ymm7, %ymm3, %ymm15 - -.second_pass: -vmovupd 96(%rsi),%xmm0 /* omri */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 /* omriri */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: omiiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: omrrrr */ -vsubpd %ymm10,%ymm8,%ymm4 -vsubpd %ymm11,%ymm9,%ymm5 -vsubpd %ymm14,%ymm12,%ymm6 -vsubpd %ymm15,%ymm13,%ymm7 -vaddpd %ymm10,%ymm8,%ymm8 -vaddpd %ymm11,%ymm9,%ymm9 -vaddpd %ymm14,%ymm12,%ymm12 -vaddpd %ymm15,%ymm13,%ymm13 -vshufpd $5, %ymm4, %ymm4, %ymm10 -vshufpd $5, %ymm5, %ymm5, %ymm11 -vshufpd $5, %ymm6, %ymm6, %ymm14 -vshufpd $5, %ymm7, %ymm7, %ymm15 -vmulpd %ymm10,%ymm1,%ymm10 -vmulpd %ymm11,%ymm1,%ymm11 -vmulpd %ymm14,%ymm0,%ymm14 -vmulpd %ymm15,%ymm0,%ymm15 -vfmaddsub231pd %ymm4, %ymm0, %ymm10 # ymm10 = (ymm0 * ymm4) +/- ymm10 -vfmaddsub231pd %ymm5, %ymm0, %ymm11 -vfmsubadd231pd %ymm6, %ymm1, %ymm14 -vfmsubadd231pd %ymm7, %ymm1, %ymm15 - -.first_pass: -vmovupd 112(%rsi),%xmm0 /* omri */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 /* omriri */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: omiiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: omrrrr */ -vsubpd %ymm12,%ymm8,%ymm4 -vsubpd %ymm13,%ymm9,%ymm5 -vsubpd %ymm14,%ymm10,%ymm6 -vsubpd %ymm15,%ymm11,%ymm7 -vaddpd %ymm12,%ymm8,%ymm8 -vaddpd %ymm13,%ymm9,%ymm9 -vaddpd %ymm14,%ymm10,%ymm10 -vaddpd %ymm15,%ymm11,%ymm11 -vshufpd $5, %ymm4, %ymm4, %ymm12 -vshufpd $5, %ymm5, %ymm5, %ymm13 -vshufpd $5, %ymm6, %ymm6, %ymm14 -vshufpd $5, %ymm7, %ymm7, %ymm15 -vmulpd %ymm12,%ymm1,%ymm12 -vmulpd %ymm13,%ymm1,%ymm13 -vmulpd %ymm14,%ymm1,%ymm14 -vmulpd %ymm15,%ymm1,%ymm15 -vfmaddsub231pd %ymm4, %ymm0, %ymm12 # ymm12 = (ymm0 * ymm4) +/- ymm12 -vfmaddsub231pd %ymm5, %ymm0, %ymm13 -vfmaddsub231pd %ymm6, %ymm0, %ymm14 -vfmaddsub231pd %ymm7, %ymm0, %ymm15 - -.save_and_return: -vmovupd %ymm8,(%rdi) -vmovupd %ymm9,0x20(%rdi) -vmovupd %ymm10,0x40(%rdi) -vmovupd %ymm11,0x60(%rdi) -vmovupd %ymm12,0x80(%rdi) -vmovupd %ymm13,0xa0(%rdi) -vmovupd %ymm14,0xc0(%rdi) -vmovupd %ymm15,0xe0(%rdi) -ret
D spqlios/lib/spqlios/cplx/cplx_ifft_avx2_fma.c

@@ -1,267 +0,0 @@

-#include <immintrin.h> -#include <math.h> -#include <string.h> - -#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -typedef double D4MEM[4]; -typedef double D2MEM[2]; - -/** - * @brief complex ifft via bfs strategy (for m between 2 and 8) - * @param dat the data to run the algorithm on - * @param omg precomputed tables (must have been filled with fill_omega) - * @param m ring dimension of the FFT (modulo X^m-i) - */ -void cplx_ifft_avx2_fma_bfs_2(D4MEM* dat, const D2MEM** omga, uint32_t m) { - double* data = (double*)dat; - D4MEM* const finaldd = (D4MEM*)(data + 2 * m); - { - // loop with h = 1 - // we do not do any particular optimization in this loop, - // since this function is only called for small dimensions - D4MEM* dd = (D4MEM*)data; - do { - /* - BEGIN_TEMPLATE - const __m256d ab% = _mm256_loadu_pd(dd[0+2*%]); - const __m256d cd% = _mm256_loadu_pd(dd[1+2*%]); - const __m256d ac% = _mm256_permute2f128_pd(ab%, cd%, 0b100000); - const __m256d bd% = _mm256_permute2f128_pd(ab%, cd%, 0b110001); - const __m256d sum% = _mm256_add_pd(ac%, bd%); - const __m256d diff% = _mm256_sub_pd(ac%, bd%); - const __m256d diffbar% = _mm256_shuffle_pd(diff%, diff%, 5); - const __m256d om% = _mm256_load_pd((*omg)[0+%]); - const __m256d omre% = _mm256_unpacklo_pd(om%, om%); - const __m256d omim% = _mm256_unpackhi_pd(om%, om%); - const __m256d t1% = _mm256_mul_pd(diffbar%, omim%); - const __m256d t2% = _mm256_fmaddsub_pd(diff%, omre%, t1%); - const __m256d newab% = _mm256_permute2f128_pd(sum%, t2%, 0b100000); - const __m256d newcd% = _mm256_permute2f128_pd(sum%, t2%, 0b110001); - _mm256_storeu_pd(dd[0+2*%], newab%); - _mm256_storeu_pd(dd[1+2*%], newcd%); - dd += 2*@; - *omg += 2*@; - END_TEMPLATE - */ - // BEGIN_INTERLEAVE 1 - const __m256d ab0 = _mm256_loadu_pd(dd[0 + 2 * 0]); - const __m256d cd0 = _mm256_loadu_pd(dd[1 + 2 * 0]); - const __m256d ac0 = _mm256_permute2f128_pd(ab0, cd0, 0b100000); - const __m256d bd0 = _mm256_permute2f128_pd(ab0, cd0, 0b110001); - const __m256d sum0 = _mm256_add_pd(ac0, bd0); - const __m256d diff0 = _mm256_sub_pd(ac0, bd0); - const __m256d diffbar0 = _mm256_shuffle_pd(diff0, diff0, 5); - const __m256d om0 = _mm256_load_pd((*omga)[0 + 0]); - const __m256d omre0 = _mm256_unpacklo_pd(om0, om0); - const __m256d omim0 = _mm256_unpackhi_pd(om0, om0); - const __m256d t10 = _mm256_mul_pd(diffbar0, omim0); - const __m256d t20 = _mm256_fmaddsub_pd(diff0, omre0, t10); - const __m256d newab0 = _mm256_permute2f128_pd(sum0, t20, 0b100000); - const __m256d newcd0 = _mm256_permute2f128_pd(sum0, t20, 0b110001); - _mm256_storeu_pd(dd[0 + 2 * 0], newab0); - _mm256_storeu_pd(dd[1 + 2 * 0], newcd0); - dd += 2 * 1; - *omga += 2 * 1; - // END_INTERLEAVE - } while (dd < finaldd); -#if 0 - printf("c after first: "); - for (uint64_t ii=0; ii<nn/2; ++ii) { - printf("%.6lf %.6lf ",ddata[ii][0],ddata[ii][1]); - } - printf("\n"); -#endif - } - // general case - const uint32_t ms2 = m >> 1; - for (uint32_t _2nblock = 2; _2nblock <= ms2; _2nblock <<= 1) { - // _2nblock = h in ref code - uint32_t nblock = _2nblock >> 1; // =h/2 in ref code - D4MEM* dd = (D4MEM*)data; - do { - const __m256d om = _mm256_load_pd((*omga)[0]); - const __m256d omre = _mm256_unpacklo_pd(om, om); - const __m256d omim = _mm256_addsub_pd(_mm256_setzero_pd(), _mm256_unpackhi_pd(om, om)); - D4MEM* const ddend = (dd + nblock); - D4MEM* ddmid = ddend; - do { - const __m256d a = _mm256_loadu_pd(dd[0]); - const __m256d b = _mm256_loadu_pd(ddmid[0]); - const __m256d newa = _mm256_add_pd(a, b); - _mm256_storeu_pd(dd[0], newa); - const __m256d diff = _mm256_sub_pd(a, b); - const __m256d t1 = _mm256_mul_pd(diff, omre); - const __m256d bardiff = _mm256_shuffle_pd(diff, diff, 5); - const __m256d t2 = _mm256_fmadd_pd(bardiff, omim, t1); - _mm256_storeu_pd(ddmid[0], t2); - dd += 1; - ddmid += 1; - } while (dd < ddend); - dd += nblock; - *omga += 2; - } while (dd < finaldd); - } -} - -/** - * @brief complex fft via bfs strategy (for m >= 16) - * @param dat the data to run the algorithm on - * @param omg precomputed tables (must have been filled with fill_omega) - * @param m ring dimension of the FFT (modulo X^m-i) - */ -void cplx_ifft_avx2_fma_bfs_16(D4MEM* dat, const D2MEM** omga, uint32_t m) { - double* data = (double*)dat; - D4MEM* const finaldd = (D4MEM*)(data + 2 * m); - // base iteration when h = _2nblock == 8 - { - D4MEM* dd = (D4MEM*)data; - do { - cplx_ifft16_avx_fma(dd, *omga); - dd += 8; - *omga += 8; - } while (dd < finaldd); - } - // general case - const uint32_t log2m = _mm_popcnt_u32(m-1); //_popcnt32(m-1); //log2(m); - uint32_t h=16; - if (log2m % 2 == 1) { - uint32_t nblock = h >> 1; // =h/2 in ref code - D4MEM* dd = (D4MEM*)data; - do { - const __m128d om1 = _mm_loadu_pd((*omga)[0]); - const __m256d om = _mm256_set_m128d(om1,om1); - const __m256d omre = _mm256_unpacklo_pd(om, om); - const __m256d omim = _mm256_addsub_pd(_mm256_setzero_pd(), _mm256_unpackhi_pd(om, om)); - D4MEM* const ddend = (dd + nblock); - D4MEM* ddmid = ddend; - do { - const __m256d a = _mm256_loadu_pd(dd[0]); - const __m256d b = _mm256_loadu_pd(ddmid[0]); - const __m256d newa = _mm256_add_pd(a, b); - _mm256_storeu_pd(dd[0], newa); - const __m256d diff = _mm256_sub_pd(a, b); - const __m256d t1 = _mm256_mul_pd(diff, omre); - const __m256d bardiff = _mm256_shuffle_pd(diff, diff, 5); - const __m256d t2 = _mm256_fmadd_pd(bardiff, omim, t1); - _mm256_storeu_pd(ddmid[0], t2); - dd += 1; - ddmid += 1; - } while (dd < ddend); - dd += nblock; - *omga += 1; - } while (dd < finaldd); - h = 32; - } - for (; h < m; h <<= 2) { - // _2nblock = h in ref code - uint32_t nblock = h >> 1; // =h/2 in ref code - D4MEM* dd0 = (D4MEM*)data; - do { - const __m128d om1 = _mm_loadu_pd((*omga)[0]); - const __m128d al1 = _mm_loadu_pd((*omga)[1]); - const __m256d om = _mm256_set_m128d(om1,om1); - const __m256d al = _mm256_set_m128d(al1,al1); - const __m256d omre = _mm256_unpacklo_pd(om, om); - const __m256d omim = _mm256_unpackhi_pd(om, om); - const __m256d alre = _mm256_unpacklo_pd(al, al); - const __m256d alim = _mm256_unpackhi_pd(al, al); - D4MEM* const ddend = (dd0 + nblock); - D4MEM* dd1 = ddend; - D4MEM* dd2 = dd1 + nblock; - D4MEM* dd3 = dd2 + nblock; - do { - __m256d u0 = _mm256_loadu_pd(dd0[0]); - __m256d u1 = _mm256_loadu_pd(dd1[0]); - __m256d u2 = _mm256_loadu_pd(dd2[0]); - __m256d u3 = _mm256_loadu_pd(dd3[0]); - __m256d u4 = _mm256_add_pd(u0, u1); - __m256d u5 = _mm256_sub_pd(u0, u1); - __m256d u6 = _mm256_add_pd(u2, u3); - __m256d u7 = _mm256_sub_pd(u2, u3); - u0 = _mm256_shuffle_pd(u5, u5, 5); - u2 = _mm256_shuffle_pd(u7, u7, 5); - u1 = _mm256_mul_pd(u0, omim); - u3 = _mm256_mul_pd(u2, omre); - u5 = _mm256_fmaddsub_pd(u5,omre, u1); - u7 = _mm256_fmsubadd_pd(u7,omim, u3); - ////// - u0 = _mm256_add_pd(u4,u6); - u1 = _mm256_add_pd(u5,u7); - u2 = _mm256_sub_pd(u4,u6); - u3 = _mm256_sub_pd(u5,u7); - u4 = _mm256_shuffle_pd(u2, u2, 5); - u5 = _mm256_shuffle_pd(u3, u3, 5); - u6 = _mm256_mul_pd(u4, alim); - u7 = _mm256_mul_pd(u5, alim); - u2 = _mm256_fmaddsub_pd(u2,alre, u6); - u3 = _mm256_fmaddsub_pd(u3,alre, u7); - /////// - _mm256_storeu_pd(dd0[0], u0); - _mm256_storeu_pd(dd1[0], u1); - _mm256_storeu_pd(dd2[0], u2); - _mm256_storeu_pd(dd3[0], u3); - dd0 += 1; - dd1 += 1; - dd2 += 1; - dd3 += 1; - } while (dd0 < ddend); - dd0 += 3*nblock; - *omga += 2; - } while (dd0 < finaldd); - } -} - -/** - * @brief complex ifft via dfs recursion (for m >= 16) - * @param dat the data to run the algorithm on - * @param omg precomputed tables (must have been filled with fill_omega) - * @param m ring dimension of the FFT (modulo X^m-i) - */ -void cplx_ifft_avx2_fma_rec_16(D4MEM* dat, const D2MEM** omga, uint32_t m) { - if (m <= 8) return cplx_ifft_avx2_fma_bfs_2(dat, omga, m); - if (m <= 2048) return cplx_ifft_avx2_fma_bfs_16(dat, omga, m); - const uint32_t _2nblock = m >> 1; // = h in ref code - const uint32_t nblock = _2nblock >> 1; // =h/2 in ref code - cplx_ifft_avx2_fma_rec_16(dat, omga, _2nblock); - cplx_ifft_avx2_fma_rec_16(dat + nblock, omga, _2nblock); - { - // final iteration - D4MEM* dd = dat; - const __m256d om = _mm256_load_pd((*omga)[0]); - const __m256d omre = _mm256_unpacklo_pd(om, om); - const __m256d omim = _mm256_addsub_pd(_mm256_setzero_pd(), _mm256_unpackhi_pd(om, om)); - D4MEM* const ddend = (dd + nblock); - D4MEM* ddmid = ddend; - do { - const __m256d a = _mm256_loadu_pd(dd[0]); - const __m256d b = _mm256_loadu_pd(ddmid[0]); - const __m256d newa = _mm256_add_pd(a, b); - _mm256_storeu_pd(dd[0], newa); - const __m256d diff = _mm256_sub_pd(a, b); - const __m256d t1 = _mm256_mul_pd(diff, omre); - const __m256d bardiff = _mm256_shuffle_pd(diff, diff, 5); - const __m256d t2 = _mm256_fmadd_pd(bardiff, omim, t1); - _mm256_storeu_pd(ddmid[0], t2); - dd += 1; - ddmid += 1; - } while (dd < ddend); - *omga += 2; - } -} - -/** - * @brief complex ifft via best strategy (for m>=1) - * @param dat the data to run the algorithm on: m complex numbers - * @param omg precomputed tables (must have been filled with fill_omega) - * @param m ring dimension of the FFT (modulo X^m-i) - */ -EXPORT void cplx_ifft_avx2_fma(const CPLX_IFFT_PRECOMP* precomp, void* d) { - const uint32_t m = precomp->m; - const D2MEM* omg = (D2MEM*)precomp->powomegas; - if (m <= 1) return; - if (m <= 8) return cplx_ifft_avx2_fma_bfs_2(d, &omg, m); - if (m <= 2048) return cplx_ifft_avx2_fma_bfs_16(d, &omg, m); - cplx_ifft_avx2_fma_rec_16(d, &omg, m); -}
D spqlios/lib/spqlios/cplx/cplx_ifft_ref.c

@@ -1,315 +0,0 @@

-#include <memory.h> - -#include "../commons_private.h" -#include "cplx_fft.h" -#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -/** @brief (a,b) <- (a+b,omegabar.(a-b)) */ -void invctwiddle(CPLX a, CPLX b, const CPLX ombar) { - double diffre = a[0] - b[0]; - double diffim = a[1] - b[1]; - a[0] = a[0] + b[0]; - a[1] = a[1] + b[1]; - b[0] = diffre * ombar[0] - diffim * ombar[1]; - b[1] = diffre * ombar[1] + diffim * ombar[0]; -} - -/** @brief (a,b) <- (a+b,-i.omegabar(a-b)) */ -void invcitwiddle(CPLX a, CPLX b, const CPLX ombar) { - double diffre = a[0] - b[0]; - double diffim = a[1] - b[1]; - a[0] = a[0] + b[0]; - a[1] = a[1] + b[1]; - //-i(x+iy)=-ix+y - b[0] = diffre * ombar[1] + diffim * ombar[0]; - b[1] = -diffre * ombar[0] + diffim * ombar[1]; -} - -/** @brief exp(-i.2pi.x) */ -void cplx_set_e2pimx(CPLX res, double x) { - res[0] = m_accurate_cos(2 * M_PI * x); - res[1] = -m_accurate_sin(2 * M_PI * x); -} -/** - * @brief this computes the sequence: 0,1/2,1/4,3/4,1/8,5/8,3/8,7/8,... - * essentially: the bits of (i+1) in lsb order on the basis (1/2^k) mod 1*/ -double fracrevbits(uint32_t i); -/** @brief fft modulo X^m-exp(i.2pi.entry+pwr) -- reference code */ -void cplx_ifft_naive(const uint32_t m, const double entry_pwr, CPLX* data) { - if (m == 1) return; - const double pom = entry_pwr / 2.; - const uint32_t h = m / 2; - CPLX cpom; - cplx_set_e2pimx(cpom, pom); - // do the recursive calls - cplx_ifft_naive(h, pom, data); - cplx_ifft_naive(h, pom + 0.5, data + h); - // apply the inverse twiddle factors - for (uint64_t i = 0; i < h; ++i) { - invctwiddle(data[i], data[i + h], cpom); - } -} - -void cplx_ifft16_precomp(const double entry_pwr, CPLX** omg) { - static const double j_pow = 1. / 8.; - static const double k_pow = 1. / 16.; - const double pom = entry_pwr / 2.; - const double pom_2 = entry_pwr / 4.; - const double pom_4 = entry_pwr / 8.; - const double pom_8 = entry_pwr / 16.; - cplx_set_e2pimx((*omg)[0], pom_8); - cplx_set_e2pimx((*omg)[1], pom_8 + j_pow); - cplx_set_e2pimx((*omg)[2], pom_8 + k_pow); - cplx_set_e2pimx((*omg)[3], pom_8 + j_pow + k_pow); - cplx_set_e2pimx((*omg)[4], pom_4); - cplx_set_e2pimx((*omg)[5], pom_4 + j_pow); - cplx_set_e2pimx((*omg)[6], pom_2); - cplx_set_e2pimx((*omg)[7], pom); - *omg += 8; -} - -/** - * @brief iFFT modulo X^16-omega^2 (in registers) - * @param data contains 16 complexes - * @param omegabar 8 complexes in this order: - * gammabar,jb.gammabar,kb.gammabar,kbjb.gammabar, - * betabar,jb.betabar,alphabar,omegabar - * alpha = sqrt(omega), beta = sqrt(alpha), gamma = sqrt(beta) - * jb = sqrt(ib), kb=sqrt(jb) - */ -void cplx_ifft16_ref(void* data, const void* omegabar) { - CPLX* d = data; - const CPLX* om = omegabar; - // fourth pass inverse - invctwiddle(d[0], d[1], om[0]); - invcitwiddle(d[2], d[3], om[0]); - invctwiddle(d[4], d[5], om[1]); - invcitwiddle(d[6], d[7], om[1]); - invctwiddle(d[8], d[9], om[2]); - invcitwiddle(d[10], d[11], om[2]); - invctwiddle(d[12], d[13], om[3]); - invcitwiddle(d[14], d[15], om[3]); - // third pass inverse - invctwiddle(d[0], d[2], om[4]); - invctwiddle(d[1], d[3], om[4]); - invcitwiddle(d[4], d[6], om[4]); - invcitwiddle(d[5], d[7], om[4]); - invctwiddle(d[8], d[10], om[5]); - invctwiddle(d[9], d[11], om[5]); - invcitwiddle(d[12], d[14], om[5]); - invcitwiddle(d[13], d[15], om[5]); - // second pass inverse - invctwiddle(d[0], d[4], om[6]); - invctwiddle(d[1], d[5], om[6]); - invctwiddle(d[2], d[6], om[6]); - invctwiddle(d[3], d[7], om[6]); - invcitwiddle(d[8], d[12], om[6]); - invcitwiddle(d[9], d[13], om[6]); - invcitwiddle(d[10], d[14], om[6]); - invcitwiddle(d[11], d[15], om[6]); - // first pass - for (uint64_t i = 0; i < 8; ++i) { - invctwiddle(d[0 + i], d[8 + i], om[7]); - } -} - -void cplx_ifft_ref_bfs_2(CPLX* dat, const CPLX** omg, uint32_t m) { - CPLX* const dend = dat + m; - for (CPLX* d = dat; d < dend; d += 2) { - split_fft_last_ref(d, (*omg)[0]); - *omg += 1; - } -#if 0 - printf("after first: "); - for (uint64_t ii=0; ii<nn/2; ++ii) { - printf("%.6lf %.6lf ",data[ii][0],data[ii][1]); - } - printf("\n"); -#endif - int32_t Ms2 = m / 2; - for (int32_t h = 2; h <= Ms2; h <<= 1) { - for (CPLX* d = dat; d < dend; d += 2 * h) { - if (memcmp((*omg)[0], (*omg)[1], 8) != 0) abort(); - cplx_split_fft_ref(h, d, **omg); - *omg += 2; - } -#if 0 - printf("after split %d: ", h); - for (uint64_t ii=0; ii<nn/2; ++ii) { - printf("%.6lf %.6lf ",data[ii][0],data[ii][1]); - } - printf("\n"); -#endif - } -} - -void cplx_ifft_ref_bfs_16(CPLX* dat, const CPLX** omg, uint32_t m) { - const uint64_t log2m = log2(m); - CPLX* const dend = dat + m; - // h=1,2,4,8 use the 16-dim macroblock - for (CPLX* d = dat; d < dend; d += 16) { - cplx_ifft16_ref(d, *omg); - *omg += 8; - } -#if 0 - printf("after first: "); - for (uint64_t ii=0; ii<nn/2; ++ii) { - printf("%.6lf %.6lf ",data[ii][0],data[ii][1]); - } - printf("\n"); -#endif - int32_t h = 16; - if (log2m % 2 != 0) { - // if parity needs it, uses one regular twiddle - for (CPLX* d = dat; d < dend; d += 2 * h) { - cplx_split_fft_ref(h, d, **omg); - *omg += 1; - } - h = 32; - } - // h=16,...,2*floor(Ms2/2) use the bitwiddle - for (; h < m; h <<= 2) { - for (CPLX* d = dat; d < dend; d += 4 * h) { - cplx_bisplit_fft_ref(h, d, *omg); - *omg += 2; - } -#if 0 - printf("after split %d: ", h); - for (uint64_t ii=0; ii<nn/2; ++ii) { - printf("%.6lf %.6lf ",data[ii][0],data[ii][1]); - } - printf("\n"); -#endif - } -} - -void fill_cplx_ifft_omegas_bfs_16(const double entry_pwr, CPLX** omg, uint32_t m) { - const uint64_t log2m = log2(m); - double pwr = entry_pwr * 16. / m; - { - // h=8 - for (uint32_t i = 0; i < m / 16; i++) { - cplx_ifft16_precomp(pwr + fracrevbits(i), omg); - } - } - int32_t h = 16; - if (log2m % 2 != 0) { - // if parity needs it, uses one regular twiddle - for (uint32_t i = 0; i < m / (2 * h); i++) { - cplx_set_e2pimx(omg[0][0], pwr + fracrevbits(i) / 2.); - *omg += 1; - } - pwr *= 2.; - h = 32; - } - for (; h < m; h <<= 2) { - for (uint32_t i = 0; i < m / (2 * h); i+=2) { - cplx_set_e2pimx(omg[0][0], pwr + fracrevbits(i) / 2.); - cplx_set_e2pimx(omg[0][1], 2.*pwr + fracrevbits(i)); - *omg += 2; - } - pwr *= 4.; - } -} - -void fill_cplx_ifft_omegas_bfs_2(const double entry_pwr, CPLX** omg, uint32_t m) { - double pom = entry_pwr / m; - { - // h=1 - for (uint32_t i = 0; i < m / 2; i++) { - cplx_set_e2pimx((*omg)[0], pom + fracrevbits(i) / 2.); - *omg += 1; // optim function reads by 1 - } - } - for (int32_t h = 2; h <= m / 2; h <<= 1) { - pom *= 2; - for (uint32_t i = 0; i < m / (2 * h); i++) { - cplx_set_e2pimx(omg[0][0], pom + fracrevbits(i) / 2.); - cplx_set(omg[0][1], omg[0][0]); - *omg += 2; - } - } -} - -void fill_cplx_ifft_omegas_rec_16(const double entry_pwr, CPLX** omg, uint32_t m) { - if (m == 1) return; - if (m <= 8) return fill_cplx_ifft_omegas_bfs_2(entry_pwr, omg, m); - if (m <= 2048) return fill_cplx_ifft_omegas_bfs_16(entry_pwr, omg, m); - double pom = entry_pwr / 2.; - uint32_t h = m / 2; - fill_cplx_ifft_omegas_rec_16(pom, omg, h); - fill_cplx_ifft_omegas_rec_16(pom + 0.5, omg, h); - cplx_set_e2pimx((*omg)[0], pom); - cplx_set((*omg)[1], (*omg)[0]); - *omg += 2; -} - -void cplx_ifft_ref_rec_16(CPLX* dat, const CPLX** omg, uint32_t m) { - if (m == 1) return; - if (m <= 8) return cplx_ifft_ref_bfs_2(dat, omg, m); - if (m <= 2048) return cplx_ifft_ref_bfs_16(dat, omg, m); - const uint32_t h = m / 2; - cplx_ifft_ref_rec_16(dat, omg, h); - cplx_ifft_ref_rec_16(dat + h, omg, h); - if (memcmp((*omg)[0], (*omg)[1], 8) != 0) abort(); - cplx_split_fft_ref(h, dat, **omg); - *omg += 2; -} - -EXPORT void cplx_ifft_ref(const CPLX_IFFT_PRECOMP* precomp, void* d) { - CPLX* data = (CPLX*)d; - const int32_t m = precomp->m; - const CPLX* omg = (CPLX*)precomp->powomegas; - if (m == 1) return; - if (m <= 8) return cplx_ifft_ref_bfs_2(data, &omg, m); - if (m <= 2048) return cplx_ifft_ref_bfs_16(data, &omg, m); - cplx_ifft_ref_rec_16(data, &omg, m); -} - -EXPORT CPLX_IFFT_PRECOMP* new_cplx_ifft_precomp(uint32_t m, uint32_t num_buffers) { - const uint64_t OMG_SPACE = ceilto64b(2 * m * sizeof(CPLX)); - const uint64_t BUF_SIZE = ceilto64b(m * sizeof(CPLX)); - void* reps = malloc(sizeof(CPLX_IFFT_PRECOMP) + 63 // padding - + OMG_SPACE // tables - + num_buffers * BUF_SIZE // buffers - ); - uint64_t aligned_addr = ceilto64b((uint64_t) reps + sizeof(CPLX_IFFT_PRECOMP)); - CPLX_IFFT_PRECOMP* r = (CPLX_IFFT_PRECOMP*)reps; - r->m = m; - r->buf_size = BUF_SIZE; - r->powomegas = (double*)aligned_addr; - r->aligned_buffers = (void*)(aligned_addr + OMG_SPACE); - // fill in powomegas - CPLX* omg = (CPLX*)r->powomegas; - fill_cplx_ifft_omegas_rec_16(0.25, &omg, m); - if (((uint64_t)omg) - aligned_addr > OMG_SPACE) abort(); - { - if (m <= 4) { - // currently, we do not have any acceletated - // implementation for m<=4 - r->function = cplx_ifft_ref; - } else if (CPU_SUPPORTS("fma")) { - r->function = cplx_ifft_avx2_fma; - } else { - r->function = cplx_ifft_ref; - } - } - return reps; -} - -EXPORT void* cplx_ifft_precomp_get_buffer(const CPLX_IFFT_PRECOMP* itables, uint32_t buffer_index) { - return (uint8_t*) itables->aligned_buffers + buffer_index * itables->buf_size; -} - -EXPORT void cplx_ifft(const CPLX_IFFT_PRECOMP* itables, void* data) { - itables->function(itables, data); -} - -EXPORT void cplx_ifft_simple(uint32_t m, void* data) { - static CPLX_IFFT_PRECOMP* p[31] = {0}; - CPLX_IFFT_PRECOMP** f = p + log2m(m); - if (!*f) *f = new_cplx_ifft_precomp(m, 0); - (*f)->function(*f, data); -} -
D spqlios/lib/spqlios/ext/neon_accel/macrof.h

@@ -1,138 +0,0 @@

-/* - * This file is extracted from the implementation of the FFT on Arm64/Neon - * available in https://github.com/cothan/Falcon-Arm (neon/macrof.h). - * ============================================================================= - * Copyright (c) 2022 by Cryptographic Engineering Research Group (CERG) - * ECE Department, George Mason University - * Fairfax, VA, U.S.A. - * @author: Duc Tri Nguyen dnguye69@gmu.edu, cothannguyen@gmail.com - * Licensed under the Apache License, Version 2.0 (the "License"); - * ============================================================================= - * - * This 64-bit Floating point NEON macro x1 has not been modified and is provided as is. - */ - -#ifndef MACROF_H -#define MACROF_H - -#include <arm_neon.h> - -// c <= addr x1 -#define vload(c, addr) c = vld1q_f64(addr); -// c <= addr interleave 2 -#define vload2(c, addr) c = vld2q_f64(addr); -// c <= addr interleave 4 -#define vload4(c, addr) c = vld4q_f64(addr); - -#define vstore(addr, c) vst1q_f64(addr, c); -// addr <= c -#define vstore2(addr, c) vst2q_f64(addr, c); -// addr <= c -#define vstore4(addr, c) vst4q_f64(addr, c); - -// c <= addr x2 -#define vloadx2(c, addr) c = vld1q_f64_x2(addr); -// c <= addr x3 -#define vloadx3(c, addr) c = vld1q_f64_x3(addr); - -// addr <= c -#define vstorex2(addr, c) vst1q_f64_x2(addr, c); - -// c = a - b -#define vfsub(c, a, b) c = vsubq_f64(a, b); - -// c = a + b -#define vfadd(c, a, b) c = vaddq_f64(a, b); - -// c = a * b -#define vfmul(c, a, b) c = vmulq_f64(a, b); - -// c = a * n (n is constant) -#define vfmuln(c, a, n) c = vmulq_n_f64(a, n); - -// Swap from a|b to b|a -#define vswap(c, a) c = vextq_f64(a, a, 1); - -// c = a * b[i] -#define vfmul_lane(c, a, b, i) c = vmulq_laneq_f64(a, b, i); - -// c = 1/a -#define vfinv(c, a) c = vdivq_f64(vdupq_n_f64(1.0), a); - -// c = -a -#define vfneg(c, a) c = vnegq_f64(a); - -#define transpose_f64(a, b, t, ia, ib, it) \ - t.val[it] = a.val[ia]; \ - a.val[ia] = vzip1q_f64(a.val[ia], b.val[ib]); \ - b.val[ib] = vzip2q_f64(t.val[it], b.val[ib]); - -/* - * c = a + jb - * c[0] = a[0] - b[1] - * c[1] = a[1] + b[0] - */ -#define vfcaddj(c, a, b) c = vcaddq_rot90_f64(a, b); - -/* - * c = a - jb - * c[0] = a[0] + b[1] - * c[1] = a[1] - b[0] - */ -#define vfcsubj(c, a, b) c = vcaddq_rot270_f64(a, b); - -// c[0] = c[0] + b[0]*a[0], c[1] = c[1] + b[1]*a[0] -#define vfcmla(c, a, b) c = vcmlaq_f64(c, a, b); - -// c[0] = c[0] - b[1]*a[1], c[1] = c[1] + b[0]*a[1] -#define vfcmla_90(c, a, b) c = vcmlaq_rot90_f64(c, a, b); - -// c[0] = c[0] - b[0]*a[0], c[1] = c[1] - b[1]*a[0] -#define vfcmla_180(c, a, b) c = vcmlaq_rot180_f64(c, a, b); - -// c[0] = c[0] + b[1]*a[1], c[1] = c[1] - b[0]*a[1] -#define vfcmla_270(c, a, b) c = vcmlaq_rot270_f64(c, a, b); - -/* - * Complex MUL: c = a*b - * c[0] = a[0]*b[0] - a[1]*b[1] - * c[1] = a[0]*b[1] + a[1]*b[0] - */ -#define FPC_CMUL(c, a, b) \ - c = vmulq_laneq_f64(b, a, 0); \ - c = vcmlaq_rot90_f64(c, a, b); - -/* - * Complex MUL: c = a * conjugate(b) = a * (b[0], -b[1]) - * c[0] = b[0]*a[0] + b[1]*a[1] - * c[1] = + b[0]*a[1] - b[1]*a[0] - */ -#define FPC_CMUL_CONJ(c, a, b) \ - c = vmulq_laneq_f64(a, b, 0); \ - c = vcmlaq_rot270_f64(c, b, a); - -#if FMA == 1 -// d = c + a *b -#define vfmla(d, c, a, b) d = vfmaq_f64(c, a, b); -// d = c - a * b -#define vfmls(d, c, a, b) d = vfmsq_f64(c, a, b); -// d = c + a * b[i] -#define vfmla_lane(d, c, a, b, i) d = vfmaq_laneq_f64(c, a, b, i); -// d = c - a * b[i] -#define vfmls_lane(d, c, a, b, i) d = vfmsq_laneq_f64(c, a, b, i); - -#else - // d = c + a *b - #define vfmla(d, c, a, b) d = vaddq_f64(c, vmulq_f64(a, b)); - // d = c - a *b - #define vfmls(d, c, a, b) d = vsubq_f64(c, vmulq_f64(a, b)); - // d = c + a * b[i] - #define vfmla_lane(d, c, a, b, i) \ - d = vaddq_f64(c, vmulq_laneq_f64(a, b, i)); - - #define vfmls_lane(d, c, a, b, i) \ - d = vsubq_f64(c, vmulq_laneq_f64(a, b, i)); - -#endif - -#endif
D spqlios/lib/spqlios/ext/neon_accel/macrofx4.h

@@ -1,428 +0,0 @@

-/* - * This file is extracted from the implementation of the FFT on Arm64/Neon - * available in https://github.com/cothan/Falcon-Arm (neon/macrof.h). - * ============================================================================= - * Copyright (c) 2022 by Cryptographic Engineering Research Group (CERG) - * ECE Department, George Mason University - * Fairfax, VA, U.S.A. - * @author: Duc Tri Nguyen dnguye69@gmu.edu, cothannguyen@gmail.com - * Licensed under the Apache License, Version 2.0 (the "License"); - * ============================================================================= - * - * This 64-bit Floating point NEON macro x4 has not been modified and is provided as is. - */ - -#ifndef MACROFX4_H -#define MACROFX4_H - -#include <arm_neon.h> -#include "macrof.h" - -#define vloadx4(c, addr) c = vld1q_f64_x4(addr); - -#define vstorex4(addr, c) vst1q_f64_x4(addr, c); - -#define vfdupx4(c, constant) \ - c.val[0] = vdupq_n_f64(constant); \ - c.val[1] = vdupq_n_f64(constant); \ - c.val[2] = vdupq_n_f64(constant); \ - c.val[3] = vdupq_n_f64(constant); - -#define vfnegx4(c, a) \ - c.val[0] = vnegq_f64(a.val[0]); \ - c.val[1] = vnegq_f64(a.val[1]); \ - c.val[2] = vnegq_f64(a.val[2]); \ - c.val[3] = vnegq_f64(a.val[3]); - -#define vfmulnx4(c, a, n) \ - c.val[0] = vmulq_n_f64(a.val[0], n); \ - c.val[1] = vmulq_n_f64(a.val[1], n); \ - c.val[2] = vmulq_n_f64(a.val[2], n); \ - c.val[3] = vmulq_n_f64(a.val[3], n); - -// c = a - b -#define vfsubx4(c, a, b) \ - c.val[0] = vsubq_f64(a.val[0], b.val[0]); \ - c.val[1] = vsubq_f64(a.val[1], b.val[1]); \ - c.val[2] = vsubq_f64(a.val[2], b.val[2]); \ - c.val[3] = vsubq_f64(a.val[3], b.val[3]); - -// c = a + b -#define vfaddx4(c, a, b) \ - c.val[0] = vaddq_f64(a.val[0], b.val[0]); \ - c.val[1] = vaddq_f64(a.val[1], b.val[1]); \ - c.val[2] = vaddq_f64(a.val[2], b.val[2]); \ - c.val[3] = vaddq_f64(a.val[3], b.val[3]); - -#define vfmulx4(c, a, b) \ - c.val[0] = vmulq_f64(a.val[0], b.val[0]); \ - c.val[1] = vmulq_f64(a.val[1], b.val[1]); \ - c.val[2] = vmulq_f64(a.val[2], b.val[2]); \ - c.val[3] = vmulq_f64(a.val[3], b.val[3]); - -#define vfmulx4_i(c, a, b) \ - c.val[0] = vmulq_f64(a.val[0], b); \ - c.val[1] = vmulq_f64(a.val[1], b); \ - c.val[2] = vmulq_f64(a.val[2], b); \ - c.val[3] = vmulq_f64(a.val[3], b); - -#define vfinvx4(c, a) \ - c.val[0] = vdivq_f64(vdupq_n_f64(1.0), a.val[0]); \ - c.val[1] = vdivq_f64(vdupq_n_f64(1.0), a.val[1]); \ - c.val[2] = vdivq_f64(vdupq_n_f64(1.0), a.val[2]); \ - c.val[3] = vdivq_f64(vdupq_n_f64(1.0), a.val[3]); - -#define vfcvtx4(c, a) \ - c.val[0] = vcvtq_f64_s64(a.val[0]); \ - c.val[1] = vcvtq_f64_s64(a.val[1]); \ - c.val[2] = vcvtq_f64_s64(a.val[2]); \ - c.val[3] = vcvtq_f64_s64(a.val[3]); - -#define vfmlax4(d, c, a, b) \ - vfmla(d.val[0], c.val[0], a.val[0], b.val[0]); \ - vfmla(d.val[1], c.val[1], a.val[1], b.val[1]); \ - vfmla(d.val[2], c.val[2], a.val[2], b.val[2]); \ - vfmla(d.val[3], c.val[3], a.val[3], b.val[3]); - -#define vfmlsx4(d, c, a, b) \ - vfmls(d.val[0], c.val[0], a.val[0], b.val[0]); \ - vfmls(d.val[1], c.val[1], a.val[1], b.val[1]); \ - vfmls(d.val[2], c.val[2], a.val[2], b.val[2]); \ - vfmls(d.val[3], c.val[3], a.val[3], b.val[3]); - -#define vfrintx4(c, a) \ - c.val[0] = vcvtnq_s64_f64(a.val[0]); \ - c.val[1] = vcvtnq_s64_f64(a.val[1]); \ - c.val[2] = vcvtnq_s64_f64(a.val[2]); \ - c.val[3] = vcvtnq_s64_f64(a.val[3]); - -/* - * Wrapper for FFT, split/merge and poly_float.c - */ - -#define FPC_MUL(d_re, d_im, a_re, a_im, b_re, b_im) \ - vfmul(d_re, a_re, b_re); \ - vfmls(d_re, d_re, a_im, b_im); \ - vfmul(d_im, a_re, b_im); \ - vfmla(d_im, d_im, a_im, b_re); - -#define FPC_MULx2(d_re, d_im, a_re, a_im, b_re, b_im) \ - vfmul(d_re.val[0], a_re.val[0], b_re.val[0]); \ - vfmls(d_re.val[0], d_re.val[0], a_im.val[0], b_im.val[0]); \ - vfmul(d_re.val[1], a_re.val[1], b_re.val[1]); \ - vfmls(d_re.val[1], d_re.val[1], a_im.val[1], b_im.val[1]); \ - vfmul(d_im.val[0], a_re.val[0], b_im.val[0]); \ - vfmla(d_im.val[0], d_im.val[0], a_im.val[0], b_re.val[0]); \ - vfmul(d_im.val[1], a_re.val[1], b_im.val[1]); \ - vfmla(d_im.val[1], d_im.val[1], a_im.val[1], b_re.val[1]); - -#define FPC_MULx4(d_re, d_im, a_re, a_im, b_re, b_im) \ - vfmul(d_re.val[0], a_re.val[0], b_re.val[0]); \ - vfmls(d_re.val[0], d_re.val[0], a_im.val[0], b_im.val[0]); \ - vfmul(d_re.val[1], a_re.val[1], b_re.val[1]); \ - vfmls(d_re.val[1], d_re.val[1], a_im.val[1], b_im.val[1]); \ - vfmul(d_re.val[2], a_re.val[2], b_re.val[2]); \ - vfmls(d_re.val[2], d_re.val[2], a_im.val[2], b_im.val[2]); \ - vfmul(d_re.val[3], a_re.val[3], b_re.val[3]); \ - vfmls(d_re.val[3], d_re.val[3], a_im.val[3], b_im.val[3]); \ - vfmul(d_im.val[0], a_re.val[0], b_im.val[0]); \ - vfmla(d_im.val[0], d_im.val[0], a_im.val[0], b_re.val[0]); \ - vfmul(d_im.val[1], a_re.val[1], b_im.val[1]); \ - vfmla(d_im.val[1], d_im.val[1], a_im.val[1], b_re.val[1]); \ - vfmul(d_im.val[2], a_re.val[2], b_im.val[2]); \ - vfmla(d_im.val[2], d_im.val[2], a_im.val[2], b_re.val[2]); \ - vfmul(d_im.val[3], a_re.val[3], b_im.val[3]); \ - vfmla(d_im.val[3], d_im.val[3], a_im.val[3], b_re.val[3]); - -#define FPC_MLA(d_re, d_im, a_re, a_im, b_re, b_im) \ - vfmla(d_re, d_re, a_re, b_re); \ - vfmls(d_re, d_re, a_im, b_im); \ - vfmla(d_im, d_im, a_re, b_im); \ - vfmla(d_im, d_im, a_im, b_re); - -#define FPC_MLAx2(d_re, d_im, a_re, a_im, b_re, b_im) \ - vfmla(d_re.val[0], d_re.val[0], a_re.val[0], b_re.val[0]); \ - vfmls(d_re.val[0], d_re.val[0], a_im.val[0], b_im.val[0]); \ - vfmla(d_re.val[1], d_re.val[1], a_re.val[1], b_re.val[1]); \ - vfmls(d_re.val[1], d_re.val[1], a_im.val[1], b_im.val[1]); \ - vfmla(d_im.val[0], d_im.val[0], a_re.val[0], b_im.val[0]); \ - vfmla(d_im.val[0], d_im.val[0], a_im.val[0], b_re.val[0]); \ - vfmla(d_im.val[1], d_im.val[1], a_re.val[1], b_im.val[1]); \ - vfmla(d_im.val[1], d_im.val[1], a_im.val[1], b_re.val[1]); - -#define FPC_MLAx4(d_re, d_im, a_re, a_im, b_re, b_im) \ - vfmla(d_re.val[0], d_re.val[0], a_re.val[0], b_re.val[0]); \ - vfmls(d_re.val[0], d_re.val[0], a_im.val[0], b_im.val[0]); \ - vfmla(d_re.val[1], d_re.val[1], a_re.val[1], b_re.val[1]); \ - vfmls(d_re.val[1], d_re.val[1], a_im.val[1], b_im.val[1]); \ - vfmla(d_re.val[2], d_re.val[2], a_re.val[2], b_re.val[2]); \ - vfmls(d_re.val[2], d_re.val[2], a_im.val[2], b_im.val[2]); \ - vfmla(d_re.val[3], d_re.val[3], a_re.val[3], b_re.val[3]); \ - vfmls(d_re.val[3], d_re.val[3], a_im.val[3], b_im.val[3]); \ - vfmla(d_im.val[0], d_im.val[0], a_re.val[0], b_im.val[0]); \ - vfmla(d_im.val[0], d_im.val[0], a_im.val[0], b_re.val[0]); \ - vfmla(d_im.val[1], d_im.val[1], a_re.val[1], b_im.val[1]); \ - vfmla(d_im.val[1], d_im.val[1], a_im.val[1], b_re.val[1]); \ - vfmla(d_im.val[2], d_im.val[2], a_re.val[2], b_im.val[2]); \ - vfmla(d_im.val[2], d_im.val[2], a_im.val[2], b_re.val[2]); \ - vfmla(d_im.val[3], d_im.val[3], a_re.val[3], b_im.val[3]); \ - vfmla(d_im.val[3], d_im.val[3], a_im.val[3], b_re.val[3]); - -#define FPC_MUL_CONJx4(d_re, d_im, a_re, a_im, b_re, b_im) \ - vfmul(d_re.val[0], b_im.val[0], a_im.val[0]); \ - vfmla(d_re.val[0], d_re.val[0], a_re.val[0], b_re.val[0]); \ - vfmul(d_re.val[1], b_im.val[1], a_im.val[1]); \ - vfmla(d_re.val[1], d_re.val[1], a_re.val[1], b_re.val[1]); \ - vfmul(d_re.val[2], b_im.val[2], a_im.val[2]); \ - vfmla(d_re.val[2], d_re.val[2], a_re.val[2], b_re.val[2]); \ - vfmul(d_re.val[3], b_im.val[3], a_im.val[3]); \ - vfmla(d_re.val[3], d_re.val[3], a_re.val[3], b_re.val[3]); \ - vfmul(d_im.val[0], b_re.val[0], a_im.val[0]); \ - vfmls(d_im.val[0], d_im.val[0], a_re.val[0], b_im.val[0]); \ - vfmul(d_im.val[1], b_re.val[1], a_im.val[1]); \ - vfmls(d_im.val[1], d_im.val[1], a_re.val[1], b_im.val[1]); \ - vfmul(d_im.val[2], b_re.val[2], a_im.val[2]); \ - vfmls(d_im.val[2], d_im.val[2], a_re.val[2], b_im.val[2]); \ - vfmul(d_im.val[3], b_re.val[3], a_im.val[3]); \ - vfmls(d_im.val[3], d_im.val[3], a_re.val[3], b_im.val[3]); - -#define FPC_MLA_CONJx4(d_re, d_im, a_re, a_im, b_re, b_im) \ - vfmla(d_re.val[0], d_re.val[0], b_im.val[0], a_im.val[0]); \ - vfmla(d_re.val[0], d_re.val[0], a_re.val[0], b_re.val[0]); \ - vfmla(d_re.val[1], d_re.val[1], b_im.val[1], a_im.val[1]); \ - vfmla(d_re.val[1], d_re.val[1], a_re.val[1], b_re.val[1]); \ - vfmla(d_re.val[2], d_re.val[2], b_im.val[2], a_im.val[2]); \ - vfmla(d_re.val[2], d_re.val[2], a_re.val[2], b_re.val[2]); \ - vfmla(d_re.val[3], d_re.val[3], b_im.val[3], a_im.val[3]); \ - vfmla(d_re.val[3], d_re.val[3], a_re.val[3], b_re.val[3]); \ - vfmla(d_im.val[0], d_im.val[0], b_re.val[0], a_im.val[0]); \ - vfmls(d_im.val[0], d_im.val[0], a_re.val[0], b_im.val[0]); \ - vfmla(d_im.val[1], d_im.val[1], b_re.val[1], a_im.val[1]); \ - vfmls(d_im.val[1], d_im.val[1], a_re.val[1], b_im.val[1]); \ - vfmla(d_im.val[2], d_im.val[2], b_re.val[2], a_im.val[2]); \ - vfmls(d_im.val[2], d_im.val[2], a_re.val[2], b_im.val[2]); \ - vfmla(d_im.val[3], d_im.val[3], b_re.val[3], a_im.val[3]); \ - vfmls(d_im.val[3], d_im.val[3], a_re.val[3], b_im.val[3]); - -#define FPC_MUL_LANE(d_re, d_im, a_re, a_im, b_re_im) \ - vfmul_lane(d_re, a_re, b_re_im, 0); \ - vfmls_lane(d_re, d_re, a_im, b_re_im, 1); \ - vfmul_lane(d_im, a_re, b_re_im, 1); \ - vfmla_lane(d_im, d_im, a_im, b_re_im, 0); - -#define FPC_MUL_LANEx4(d_re, d_im, a_re, a_im, b_re_im) \ - vfmul_lane(d_re.val[0], a_re.val[0], b_re_im, 0); \ - vfmls_lane(d_re.val[0], d_re.val[0], a_im.val[0], b_re_im, 1); \ - vfmul_lane(d_re.val[1], a_re.val[1], b_re_im, 0); \ - vfmls_lane(d_re.val[1], d_re.val[1], a_im.val[1], b_re_im, 1); \ - vfmul_lane(d_re.val[2], a_re.val[2], b_re_im, 0); \ - vfmls_lane(d_re.val[2], d_re.val[2], a_im.val[2], b_re_im, 1); \ - vfmul_lane(d_re.val[3], a_re.val[3], b_re_im, 0); \ - vfmls_lane(d_re.val[3], d_re.val[3], a_im.val[3], b_re_im, 1); \ - vfmul_lane(d_im.val[0], a_re.val[0], b_re_im, 1); \ - vfmla_lane(d_im.val[0], d_im.val[0], a_im.val[0], b_re_im, 0); \ - vfmul_lane(d_im.val[1], a_re.val[1], b_re_im, 1); \ - vfmla_lane(d_im.val[1], d_im.val[1], a_im.val[1], b_re_im, 0); \ - vfmul_lane(d_im.val[2], a_re.val[2], b_re_im, 1); \ - vfmla_lane(d_im.val[2], d_im.val[2], a_im.val[2], b_re_im, 0); \ - vfmul_lane(d_im.val[3], a_re.val[3], b_re_im, 1); \ - vfmla_lane(d_im.val[3], d_im.val[3], a_im.val[3], b_re_im, 0); - -#define FWD_TOP(t_re, t_im, b_re, b_im, zeta_re, zeta_im) \ - FPC_MUL(t_re, t_im, b_re, b_im, zeta_re, zeta_im); - -#define FWD_TOP_LANE(t_re, t_im, b_re, b_im, zeta) \ - FPC_MUL_LANE(t_re, t_im, b_re, b_im, zeta); - -#define FWD_TOP_LANEx4(t_re, t_im, b_re, b_im, zeta) \ - FPC_MUL_LANEx4(t_re, t_im, b_re, b_im, zeta); - -/* - * FPC - */ - -#define FPC_SUB(d_re, d_im, a_re, a_im, b_re, b_im) \ - d_re = vsubq_f64(a_re, b_re); \ - d_im = vsubq_f64(a_im, b_im); - -#define FPC_SUBx4(d_re, d_im, a_re, a_im, b_re, b_im) \ - d_re.val[0] = vsubq_f64(a_re.val[0], b_re.val[0]); \ - d_im.val[0] = vsubq_f64(a_im.val[0], b_im.val[0]); \ - d_re.val[1] = vsubq_f64(a_re.val[1], b_re.val[1]); \ - d_im.val[1] = vsubq_f64(a_im.val[1], b_im.val[1]); \ - d_re.val[2] = vsubq_f64(a_re.val[2], b_re.val[2]); \ - d_im.val[2] = vsubq_f64(a_im.val[2], b_im.val[2]); \ - d_re.val[3] = vsubq_f64(a_re.val[3], b_re.val[3]); \ - d_im.val[3] = vsubq_f64(a_im.val[3], b_im.val[3]); - -#define FPC_ADD(d_re, d_im, a_re, a_im, b_re, b_im) \ - d_re = vaddq_f64(a_re, b_re); \ - d_im = vaddq_f64(a_im, b_im); - -#define FPC_ADDx4(d_re, d_im, a_re, a_im, b_re, b_im) \ - d_re.val[0] = vaddq_f64(a_re.val[0], b_re.val[0]); \ - d_im.val[0] = vaddq_f64(a_im.val[0], b_im.val[0]); \ - d_re.val[1] = vaddq_f64(a_re.val[1], b_re.val[1]); \ - d_im.val[1] = vaddq_f64(a_im.val[1], b_im.val[1]); \ - d_re.val[2] = vaddq_f64(a_re.val[2], b_re.val[2]); \ - d_im.val[2] = vaddq_f64(a_im.val[2], b_im.val[2]); \ - d_re.val[3] = vaddq_f64(a_re.val[3], b_re.val[3]); \ - d_im.val[3] = vaddq_f64(a_im.val[3], b_im.val[3]); - -#define FWD_BOT(a_re, a_im, b_re, b_im, t_re, t_im) \ - FPC_SUB(b_re, b_im, a_re, a_im, t_re, t_im); \ - FPC_ADD(a_re, a_im, a_re, a_im, t_re, t_im); - -#define FWD_BOTx4(a_re, a_im, b_re, b_im, t_re, t_im) \ - FPC_SUBx4(b_re, b_im, a_re, a_im, t_re, t_im); \ - FPC_ADDx4(a_re, a_im, a_re, a_im, t_re, t_im); - -/* - * FPC_J - */ - -#define FPC_ADDJ(d_re, d_im, a_re, a_im, b_re, b_im) \ - d_re = vsubq_f64(a_re, b_im); \ - d_im = vaddq_f64(a_im, b_re); - -#define FPC_ADDJx4(d_re, d_im, a_re, a_im, b_re, b_im) \ - d_re.val[0] = vsubq_f64(a_re.val[0], b_im.val[0]); \ - d_im.val[0] = vaddq_f64(a_im.val[0], b_re.val[0]); \ - d_re.val[1] = vsubq_f64(a_re.val[1], b_im.val[1]); \ - d_im.val[1] = vaddq_f64(a_im.val[1], b_re.val[1]); \ - d_re.val[2] = vsubq_f64(a_re.val[2], b_im.val[2]); \ - d_im.val[2] = vaddq_f64(a_im.val[2], b_re.val[2]); \ - d_re.val[3] = vsubq_f64(a_re.val[3], b_im.val[3]); \ - d_im.val[3] = vaddq_f64(a_im.val[3], b_re.val[3]); - -#define FPC_SUBJ(d_re, d_im, a_re, a_im, b_re, b_im) \ - d_re = vaddq_f64(a_re, b_im); \ - d_im = vsubq_f64(a_im, b_re); - -#define FPC_SUBJx4(d_re, d_im, a_re, a_im, b_re, b_im) \ - d_re.val[0] = vaddq_f64(a_re.val[0], b_im.val[0]); \ - d_im.val[0] = vsubq_f64(a_im.val[0], b_re.val[0]); \ - d_re.val[1] = vaddq_f64(a_re.val[1], b_im.val[1]); \ - d_im.val[1] = vsubq_f64(a_im.val[1], b_re.val[1]); \ - d_re.val[2] = vaddq_f64(a_re.val[2], b_im.val[2]); \ - d_im.val[2] = vsubq_f64(a_im.val[2], b_re.val[2]); \ - d_re.val[3] = vaddq_f64(a_re.val[3], b_im.val[3]); \ - d_im.val[3] = vsubq_f64(a_im.val[3], b_re.val[3]); - -#define FWD_BOTJ(a_re, a_im, b_re, b_im, t_re, t_im) \ - FPC_SUBJ(b_re, b_im, a_re, a_im, t_re, t_im); \ - FPC_ADDJ(a_re, a_im, a_re, a_im, t_re, t_im); - -#define FWD_BOTJx4(a_re, a_im, b_re, b_im, t_re, t_im) \ - FPC_SUBJx4(b_re, b_im, a_re, a_im, t_re, t_im); \ - FPC_ADDJx4(a_re, a_im, a_re, a_im, t_re, t_im); - -//============== Inverse FFT -/* - * FPC_J - * a * conj(b) - * Original (without swap): - * d_re = b_im * a_im + a_re * b_re; - * d_im = b_re * a_im - a_re * b_im; - */ -#define FPC_MUL_BOTJ_LANE(d_re, d_im, a_re, a_im, b_re_im) \ - vfmul_lane(d_re, a_re, b_re_im, 0); \ - vfmla_lane(d_re, d_re, a_im, b_re_im, 1); \ - vfmul_lane(d_im, a_im, b_re_im, 0); \ - vfmls_lane(d_im, d_im, a_re, b_re_im, 1); - -#define FPC_MUL_BOTJ_LANEx4(d_re, d_im, a_re, a_im, b_re_im) \ - vfmul_lane(d_re.val[0], a_re.val[0], b_re_im, 0); \ - vfmla_lane(d_re.val[0], d_re.val[0], a_im.val[0], b_re_im, 1); \ - vfmul_lane(d_im.val[0], a_im.val[0], b_re_im, 0); \ - vfmls_lane(d_im.val[0], d_im.val[0], a_re.val[0], b_re_im, 1); \ - vfmul_lane(d_re.val[1], a_re.val[1], b_re_im, 0); \ - vfmla_lane(d_re.val[1], d_re.val[1], a_im.val[1], b_re_im, 1); \ - vfmul_lane(d_im.val[1], a_im.val[1], b_re_im, 0); \ - vfmls_lane(d_im.val[1], d_im.val[1], a_re.val[1], b_re_im, 1); \ - vfmul_lane(d_re.val[2], a_re.val[2], b_re_im, 0); \ - vfmla_lane(d_re.val[2], d_re.val[2], a_im.val[2], b_re_im, 1); \ - vfmul_lane(d_im.val[2], a_im.val[2], b_re_im, 0); \ - vfmls_lane(d_im.val[2], d_im.val[2], a_re.val[2], b_re_im, 1); \ - vfmul_lane(d_re.val[3], a_re.val[3], b_re_im, 0); \ - vfmla_lane(d_re.val[3], d_re.val[3], a_im.val[3], b_re_im, 1); \ - vfmul_lane(d_im.val[3], a_im.val[3], b_re_im, 0); \ - vfmls_lane(d_im.val[3], d_im.val[3], a_re.val[3], b_re_im, 1); - -#define FPC_MUL_BOTJ(d_re, d_im, a_re, a_im, b_re, b_im) \ - vfmul(d_re, b_im, a_im); \ - vfmla(d_re, d_re, a_re, b_re); \ - vfmul(d_im, b_re, a_im); \ - vfmls(d_im, d_im, a_re, b_im); - -#define INV_TOPJ(t_re, t_im, a_re, a_im, b_re, b_im) \ - FPC_SUB(t_re, t_im, a_re, a_im, b_re, b_im); \ - FPC_ADD(a_re, a_im, a_re, a_im, b_re, b_im); - -#define INV_TOPJx4(t_re, t_im, a_re, a_im, b_re, b_im) \ - FPC_SUBx4(t_re, t_im, a_re, a_im, b_re, b_im); \ - FPC_ADDx4(a_re, a_im, a_re, a_im, b_re, b_im); - -#define INV_BOTJ(b_re, b_im, t_re, t_im, zeta_re, zeta_im) \ - FPC_MUL_BOTJ(b_re, b_im, t_re, t_im, zeta_re, zeta_im); - -#define INV_BOTJ_LANE(b_re, b_im, t_re, t_im, zeta) \ - FPC_MUL_BOTJ_LANE(b_re, b_im, t_re, t_im, zeta); - -#define INV_BOTJ_LANEx4(b_re, b_im, t_re, t_im, zeta) \ - FPC_MUL_BOTJ_LANEx4(b_re, b_im, t_re, t_im, zeta); - -/* - * FPC_Jm - * a * -conj(b) - * d_re = a_re * b_im - a_im * b_re; - * d_im = a_im * b_im + a_re * b_re; - */ -#define FPC_MUL_BOTJm_LANE(d_re, d_im, a_re, a_im, b_re_im) \ - vfmul_lane(d_re, a_re, b_re_im, 1); \ - vfmls_lane(d_re, d_re, a_im, b_re_im, 0); \ - vfmul_lane(d_im, a_re, b_re_im, 0); \ - vfmla_lane(d_im, d_im, a_im, b_re_im, 1); - -#define FPC_MUL_BOTJm_LANEx4(d_re, d_im, a_re, a_im, b_re_im) \ - vfmul_lane(d_re.val[0], a_re.val[0], b_re_im, 1); \ - vfmls_lane(d_re.val[0], d_re.val[0], a_im.val[0], b_re_im, 0); \ - vfmul_lane(d_im.val[0], a_re.val[0], b_re_im, 0); \ - vfmla_lane(d_im.val[0], d_im.val[0], a_im.val[0], b_re_im, 1); \ - vfmul_lane(d_re.val[1], a_re.val[1], b_re_im, 1); \ - vfmls_lane(d_re.val[1], d_re.val[1], a_im.val[1], b_re_im, 0); \ - vfmul_lane(d_im.val[1], a_re.val[1], b_re_im, 0); \ - vfmla_lane(d_im.val[1], d_im.val[1], a_im.val[1], b_re_im, 1); \ - vfmul_lane(d_re.val[2], a_re.val[2], b_re_im, 1); \ - vfmls_lane(d_re.val[2], d_re.val[2], a_im.val[2], b_re_im, 0); \ - vfmul_lane(d_im.val[2], a_re.val[2], b_re_im, 0); \ - vfmla_lane(d_im.val[2], d_im.val[2], a_im.val[2], b_re_im, 1); \ - vfmul_lane(d_re.val[3], a_re.val[3], b_re_im, 1); \ - vfmls_lane(d_re.val[3], d_re.val[3], a_im.val[3], b_re_im, 0); \ - vfmul_lane(d_im.val[3], a_re.val[3], b_re_im, 0); \ - vfmla_lane(d_im.val[3], d_im.val[3], a_im.val[3], b_re_im, 1); - -#define FPC_MUL_BOTJm(d_re, d_im, a_re, a_im, b_re, b_im) \ - vfmul(d_re, a_re, b_im); \ - vfmls(d_re, d_re, a_im, b_re); \ - vfmul(d_im, a_im, b_im); \ - vfmla(d_im, d_im, a_re, b_re); - -#define INV_TOPJm(t_re, t_im, a_re, a_im, b_re, b_im) \ - FPC_SUB(t_re, t_im, b_re, b_im, a_re, a_im); \ - FPC_ADD(a_re, a_im, a_re, a_im, b_re, b_im); - -#define INV_TOPJmx4(t_re, t_im, a_re, a_im, b_re, b_im) \ - FPC_SUBx4(t_re, t_im, b_re, b_im, a_re, a_im); \ - FPC_ADDx4(a_re, a_im, a_re, a_im, b_re, b_im); - -#define INV_BOTJm(b_re, b_im, t_re, t_im, zeta_re, zeta_im) \ - FPC_MUL_BOTJm(b_re, b_im, t_re, t_im, zeta_re, zeta_im); - -#define INV_BOTJm_LANE(b_re, b_im, t_re, t_im, zeta) \ - FPC_MUL_BOTJm_LANE(b_re, b_im, t_re, t_im, zeta); - -#define INV_BOTJm_LANEx4(b_re, b_im, t_re, t_im, zeta) \ - FPC_MUL_BOTJm_LANEx4(b_re, b_im, t_re, t_im, zeta); - -#endif
D spqlios/lib/spqlios/q120/q120_arithmetic.h

@@ -1,115 +0,0 @@

-#ifndef SPQLIOS_Q120_ARITHMETIC_H -#define SPQLIOS_Q120_ARITHMETIC_H - -#include <stdint.h> - -#include "../commons.h" -#include "q120_common.h" - -typedef struct _q120_mat1col_product_baa_precomp q120_mat1col_product_baa_precomp; -typedef struct _q120_mat1col_product_bbb_precomp q120_mat1col_product_bbb_precomp; -typedef struct _q120_mat1col_product_bbc_precomp q120_mat1col_product_bbc_precomp; - -EXPORT q120_mat1col_product_baa_precomp* q120_new_vec_mat1col_product_baa_precomp(); -EXPORT void q120_delete_vec_mat1col_product_baa_precomp(q120_mat1col_product_baa_precomp*); -EXPORT q120_mat1col_product_bbb_precomp* q120_new_vec_mat1col_product_bbb_precomp(); -EXPORT void q120_delete_vec_mat1col_product_bbb_precomp(q120_mat1col_product_bbb_precomp*); -EXPORT q120_mat1col_product_bbc_precomp* q120_new_vec_mat1col_product_bbc_precomp(); -EXPORT void q120_delete_vec_mat1col_product_bbc_precomp(q120_mat1col_product_bbc_precomp*); - -// ell < 10000 -EXPORT void q120_vec_mat1col_product_baa_ref(q120_mat1col_product_baa_precomp*, const uint64_t ell, q120b* const res, - const q120a* const x, const q120a* const y); -EXPORT void q120_vec_mat1col_product_bbb_ref(q120_mat1col_product_bbb_precomp*, const uint64_t ell, q120b* const res, - const q120b* const x, const q120b* const y); -EXPORT void q120_vec_mat1col_product_bbc_ref(q120_mat1col_product_bbc_precomp*, const uint64_t ell, q120b* const res, - const q120b* const x, const q120c* const y); - -EXPORT void q120_vec_mat1col_product_baa_avx2(q120_mat1col_product_baa_precomp*, const uint64_t ell, q120b* const res, - const q120a* const x, const q120a* const y); -EXPORT void q120_vec_mat1col_product_bbb_avx2(q120_mat1col_product_bbb_precomp*, const uint64_t ell, q120b* const res, - const q120b* const x, const q120b* const y); -EXPORT void q120_vec_mat1col_product_bbc_avx2(q120_mat1col_product_bbc_precomp*, const uint64_t ell, q120b* const res, - const q120b* const x, const q120c* const y); - -EXPORT void q120x2_vec_mat1col_product_bbc_ref(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y); -EXPORT void q120x2_vec_mat1col_product_bbc_avx2(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y); -EXPORT void q120x2_vec_mat2cols_product_bbc_ref(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y); -EXPORT void q120x2_vec_mat2cols_product_bbc_avx2(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y); - -/** - * @brief extract 1 q120x2 block from one q120 ntt vectors - * @param nn the size of each vector - * @param blk the block id to extract (<nn/2) - * @param dst the output: nrows q120x2's dst[i] = src[i](blk) - * @param src the input: nrows q120 ntt vecs's - */ -EXPORT void q120x2_extract_1blk_from_q120b_ref(uint64_t nn, uint64_t blk, - q120x2b* const dst, // 8 doubles - const q120b* const src // a reim vector -); -EXPORT void q120x2_extract_1blk_from_q120c_ref(uint64_t nn, uint64_t blk, - q120x2c* const dst, // 8 doubles - const q120c* const src // a reim vector -); -EXPORT void q120x2_extract_1blk_from_q120b_avx(uint64_t nn, uint64_t blk, - q120x2b* const dst, // 8 doubles - const q120b* const src // a reim vector -); -EXPORT void q120x2_extract_1blk_from_q120c_avx(uint64_t nn, uint64_t blk, - q120x2c* const dst, // 8 doubles - const q120c* const src // a reim vector -); - -/** - * @brief extract 1 reim4 block from nrows reim vectors of m complexes - * @param nn the size of each q120 - * @param nrows the number of q120 (ntt) vectors - * @param blk the block id to extract (<m/4) - * @param dst the output: nrows q120x2's dst[i] = src[i](blk) - * @param src the input: nrows q120 ntt vectors - */ -EXPORT void q120x2_extract_1blk_from_contiguous_q120b_ref( - uint64_t nn, uint64_t nrows, uint64_t blk, - q120x2b* const dst, // nrows * 2 q120 - const q120b* const src // a contiguous array of nrows q120b vectors -); -EXPORT void q120x2_extract_1blk_from_contiguous_q120b_avx( - uint64_t nn, uint64_t nrows, uint64_t blk, - q120x2b* const dst, // nrows * 2 q120 - const q120b* const src // a contiguous array of nrows q120b vectors -); - -/** - * @brief saves 1 single q120x2 block in a q120 vectors of size nn - * @param nn the size of the output q120 - * @param blk the block id to save (<nn/2) - * @param dest the output q120b vector: dst(blk) = src - * @param src the input q120x2b - */ -EXPORT void q120x2b_save_1blk_to_q120b_ref(uint64_t nn, uint64_t blk, - q120b* dest, // 1 reim vector of length m - const q120x2b* src // 8 doubles -); -EXPORT void q120x2b_save_1blk_to_q120b_avx(uint64_t nn, uint64_t blk, - q120b* dest, // 1 reim vector of length m - const q120x2b* src // 8 doubles -); - -EXPORT void q120_add_bbb_simple(uint64_t nn, q120b* const res, const q120b* const x, const q120b* const y); - -EXPORT void q120_add_ccc_simple(uint64_t nn, q120c* const res, const q120c* const x, const q120c* const y); - -EXPORT void q120_c_from_b_simple(uint64_t nn, q120c* const res, const q120b* const x); - -EXPORT void q120_b_from_znx64_simple(uint64_t nn, q120b* const res, const int64_t* const x); - -EXPORT void q120_c_from_znx64_simple(uint64_t nn, q120c* const res, const int64_t* const x); - -EXPORT void q120_b_to_znx128_simple(uint64_t nn, __int128_t* const res, const q120b* const x); - -#endif // SPQLIOS_Q120_ARITHMETIC_H
D spqlios/lib/spqlios/q120/q120_arithmetic_avx2.c

@@ -1,567 +0,0 @@

-#include <immintrin.h> - -#include "q120_arithmetic.h" -#include "q120_arithmetic_private.h" - -EXPORT void q120_vec_mat1col_product_baa_avx2(q120_mat1col_product_baa_precomp* precomp, const uint64_t ell, - q120b* const res, const q120a* const x, const q120a* const y) { - /** - * Algorithm: - * - res = acc1 + acc2 . ((2^H) % Q) - * - acc1 is the sum of H LSB of products x[i].y[i] - * - acc2 is the sum of 64-H MSB of products x[i]].y[i] - * - for l < 10k acc1 will have H + log2(10000) and acc2 64 - H + log2(10000) bits - * - final sum has max(H, 64 - H + bit_size((2^H) % Q)) + log2(10000) + 1 bits - */ - - const uint64_t H = precomp->h; - const __m256i MASK = _mm256_set1_epi64x((UINT64_C(1) << H) - 1); - - __m256i acc1 = _mm256_setzero_si256(); - __m256i acc2 = _mm256_setzero_si256(); - - const __m256i* x_ptr = (__m256i*)x; - const __m256i* y_ptr = (__m256i*)y; - - for (uint64_t i = 0; i < ell; ++i) { - __m256i a = _mm256_loadu_si256(x_ptr); - __m256i b = _mm256_loadu_si256(y_ptr); - __m256i t = _mm256_mul_epu32(a, b); - - acc1 = _mm256_add_epi64(acc1, _mm256_and_si256(t, MASK)); - acc2 = _mm256_add_epi64(acc2, _mm256_srli_epi64(t, H)); - - x_ptr++; - y_ptr++; - } - - const __m256i H_POW_RED = _mm256_loadu_si256((__m256i*)precomp->h_pow_red); - - __m256i t = _mm256_add_epi64(acc1, _mm256_mul_epu32(acc2, H_POW_RED)); - _mm256_storeu_si256((__m256i*)res, t); -} - -EXPORT void q120_vec_mat1col_product_bbb_avx2(q120_mat1col_product_bbb_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120b* const y) { - /** - * Algorithm: - * 1. Split x_i and y_i in 2 32-bit parts and compute the cross-products: - * - x_i = xl_i + xh_i . 2^32 - * - y_i = yl_i + yh_i . 2^32 - * - A_i = xl_i . yl_i - * - B_i = xl_i . yh_i - * - C_i = xh_i . yl_i - * - D_i = xh_i . yh_i - * - we have x_i . y_i == A_i + (B_i + C_i) . 2^32 + D_i . 2^64 - * 2. Split A_i, B_i, C_i and D_i into 2 32-bit parts - * - A_i = Al_i + Ah_i . 2^32 - * - B_i = Bl_i + Bh_i . 2^32 - * - C_i = Cl_i + Ch_i . 2^32 - * - D_i = Dl_i + Dh_i . 2^32 - * 3. Compute the sums: - * - S1 = \sum Al_i - * - S2 = \sum (Ah_i + Bl_i + Cl_i) - * - S3 = \sum (Bh_i + Ch_i + Dl_i) - * - S4 = \sum Dh_i - * - here S1, S4 have 32 + log2(ell) bits and S2, S3 have 32 + log2(ell) + - * log2(3) bits - * - for ell == 10000 S2, S3 have < 47 bits - * 4. Split S1, S2, S3 and S4 in 2 24-bit parts (24 = ceil(47/2)) - * - S1 = S1l + S1h . 2^24 - * - S2 = S2l + S2h . 2^24 - * - S3 = S3l + S3h . 2^24 - * - S4 = S4l + S4h . 2^24 - * 5. Compute final result as: - * - \sum x_i . y_i = S1l + S1h . 2^24 - * + S2l . 2^32 + S2h . 2^(32+24) - * + S3l . 2^64 + S3h . 2^(64 + 24) - * + S4l . 2^96 + S4l . 2^(96+24) - * - here the powers of 2 are reduced modulo the primes Q before - * multiplications - * - the result will be on 24 + 3 + bit size of primes Q - */ - const uint64_t H1 = 32; - const __m256i MASK1 = _mm256_set1_epi64x((UINT64_C(1) << H1) - 1); - - __m256i s1 = _mm256_setzero_si256(); - __m256i s2 = _mm256_setzero_si256(); - __m256i s3 = _mm256_setzero_si256(); - __m256i s4 = _mm256_setzero_si256(); - - const __m256i* x_ptr = (__m256i*)x; - const __m256i* y_ptr = (__m256i*)y; - - for (uint64_t i = 0; i < ell; ++i) { - __m256i x = _mm256_loadu_si256(x_ptr); - __m256i xl = _mm256_and_si256(x, MASK1); - __m256i xh = _mm256_srli_epi64(x, H1); - - __m256i y = _mm256_loadu_si256(y_ptr); - __m256i yl = _mm256_and_si256(y, MASK1); - __m256i yh = _mm256_srli_epi64(y, H1); - - __m256i a = _mm256_mul_epu32(xl, yl); - __m256i b = _mm256_mul_epu32(xl, yh); - __m256i c = _mm256_mul_epu32(xh, yl); - __m256i d = _mm256_mul_epu32(xh, yh); - - s1 = _mm256_add_epi64(s1, _mm256_and_si256(a, MASK1)); - - s2 = _mm256_add_epi64(s2, _mm256_srli_epi64(a, H1)); - s2 = _mm256_add_epi64(s2, _mm256_and_si256(b, MASK1)); - s2 = _mm256_add_epi64(s2, _mm256_and_si256(c, MASK1)); - - s3 = _mm256_add_epi64(s3, _mm256_srli_epi64(b, H1)); - s3 = _mm256_add_epi64(s3, _mm256_srli_epi64(c, H1)); - s3 = _mm256_add_epi64(s3, _mm256_and_si256(d, MASK1)); - - s4 = _mm256_add_epi64(s4, _mm256_srli_epi64(d, H1)); - - x_ptr++; - y_ptr++; - } - - const uint64_t H2 = precomp->h; - const __m256i MASK2 = _mm256_set1_epi64x((UINT64_C(1) << H2) - 1); - - const __m256i S1H_POW_RED = _mm256_loadu_si256((__m256i*)precomp->s1h_pow_red); - __m256i s1l = _mm256_and_si256(s1, MASK2); - __m256i s1h = _mm256_srli_epi64(s1, H2); - __m256i t = _mm256_add_epi64(s1l, _mm256_mul_epu32(s1h, S1H_POW_RED)); - - const __m256i S2L_POW_RED = _mm256_loadu_si256((__m256i*)precomp->s2l_pow_red); - const __m256i S2H_POW_RED = _mm256_loadu_si256((__m256i*)precomp->s2h_pow_red); - __m256i s2l = _mm256_and_si256(s2, MASK2); - __m256i s2h = _mm256_srli_epi64(s2, H2); - t = _mm256_add_epi64(t, _mm256_mul_epu32(s2l, S2L_POW_RED)); - t = _mm256_add_epi64(t, _mm256_mul_epu32(s2h, S2H_POW_RED)); - - const __m256i S3L_POW_RED = _mm256_loadu_si256((__m256i*)precomp->s3l_pow_red); - const __m256i S3H_POW_RED = _mm256_loadu_si256((__m256i*)precomp->s3h_pow_red); - __m256i s3l = _mm256_and_si256(s3, MASK2); - __m256i s3h = _mm256_srli_epi64(s3, H2); - t = _mm256_add_epi64(t, _mm256_mul_epu32(s3l, S3L_POW_RED)); - t = _mm256_add_epi64(t, _mm256_mul_epu32(s3h, S3H_POW_RED)); - - const __m256i S4L_POW_RED = _mm256_loadu_si256((__m256i*)precomp->s4l_pow_red); - const __m256i S4H_POW_RED = _mm256_loadu_si256((__m256i*)precomp->s4h_pow_red); - __m256i s4l = _mm256_and_si256(s4, MASK2); - __m256i s4h = _mm256_srli_epi64(s4, H2); - t = _mm256_add_epi64(t, _mm256_mul_epu32(s4l, S4L_POW_RED)); - t = _mm256_add_epi64(t, _mm256_mul_epu32(s4h, S4H_POW_RED)); - - _mm256_storeu_si256((__m256i*)res, t); -} - -EXPORT void q120_vec_mat1col_product_bbc_avx2(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y) { - /** - * Algorithm: - * 0. We have - * - y0_i == y_i % Q and y1_i == (y_i . 2^32) % Q - * 1. Split x_i in 2 32-bit parts and compute the cross-products: - * - x_i = xl_i + xh_i . 2^32 - * - A_i = xl_i . y1_i - * - B_i = xh_i . y2_i - * - we have x_i . y_i == A_i + B_i - * 2. Split A_i and B_i into 2 32-bit parts - * - A_i = Al_i + Ah_i . 2^32 - * - B_i = Bl_i + Bh_i . 2^32 - * 3. Compute the sums: - * - S1 = \sum Al_i + Bl_i - * - S2 = \sum Ah_i + Bh_i - * - here S1 and S2 have 32 + log2(ell) bits - * - for ell == 10000 S1, S2 have < 46 bits - * 4. Split S2 in 27-bit and 19-bit parts (27+19 == 46) - * - S2 = S2l + S2h . 2^27 - * 5. Compute final result as: - * - \sum x_i . y_i = S1 + S2l . 2^32 + S2h . 2^(32+27) - * - here the powers of 2 are reduced modulo the primes Q before - * multiplications - * - the result will be on < 52 bits - */ - - const uint64_t H1 = 32; - const __m256i MASK1 = _mm256_set1_epi64x((UINT64_C(1) << H1) - 1); - - __m256i s1 = _mm256_setzero_si256(); - __m256i s2 = _mm256_setzero_si256(); - - const __m256i* x_ptr = (__m256i*)x; - const __m256i* y_ptr = (__m256i*)y; - - for (uint64_t i = 0; i < ell; ++i) { - __m256i x = _mm256_loadu_si256(x_ptr); - __m256i xl = _mm256_and_si256(x, MASK1); - __m256i xh = _mm256_srli_epi64(x, H1); - - __m256i y = _mm256_loadu_si256(y_ptr); - __m256i y0 = _mm256_and_si256(y, MASK1); - __m256i y1 = _mm256_srli_epi64(y, H1); - - __m256i a = _mm256_mul_epu32(xl, y0); - __m256i b = _mm256_mul_epu32(xh, y1); - - s1 = _mm256_add_epi64(s1, _mm256_and_si256(a, MASK1)); - s1 = _mm256_add_epi64(s1, _mm256_and_si256(b, MASK1)); - - s2 = _mm256_add_epi64(s2, _mm256_srli_epi64(a, H1)); - s2 = _mm256_add_epi64(s2, _mm256_srli_epi64(b, H1)); - - x_ptr++; - y_ptr++; - } - - const uint64_t H2 = precomp->h; - const __m256i MASK2 = _mm256_set1_epi64x((UINT64_C(1) << H2) - 1); - - __m256i t = s1; - - const __m256i S2L_POW_RED = _mm256_loadu_si256((__m256i*)precomp->s2l_pow_red); - const __m256i S2H_POW_RED = _mm256_loadu_si256((__m256i*)precomp->s2h_pow_red); - __m256i s2l = _mm256_and_si256(s2, MASK2); - __m256i s2h = _mm256_srli_epi64(s2, H2); - t = _mm256_add_epi64(t, _mm256_mul_epu32(s2l, S2L_POW_RED)); - t = _mm256_add_epi64(t, _mm256_mul_epu32(s2h, S2H_POW_RED)); - - _mm256_storeu_si256((__m256i*)res, t); -} - -/** - * @deprecated keeping this one for history only. - * There is a slight register starvation condition on the q120x2_vec_mat2cols - * strategy below sounds better. - */ -EXPORT void q120x2_vec_mat2cols_product_bbc_avx2_old(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y) { - __m256i s0 = _mm256_setzero_si256(); // col 1a - __m256i s1 = _mm256_setzero_si256(); - __m256i s2 = _mm256_setzero_si256(); // col 1b - __m256i s3 = _mm256_setzero_si256(); - __m256i s4 = _mm256_setzero_si256(); // col 2a - __m256i s5 = _mm256_setzero_si256(); - __m256i s6 = _mm256_setzero_si256(); // col 2b - __m256i s7 = _mm256_setzero_si256(); - __m256i s8, s9, s10, s11; - __m256i s12, s13, s14, s15; - - const __m256i* x_ptr = (__m256i*)x; - const __m256i* y_ptr = (__m256i*)y; - __m256i* res_ptr = (__m256i*)res; - for (uint64_t i = 0; i < ell; ++i) { - s8 = _mm256_loadu_si256(x_ptr); - s9 = _mm256_loadu_si256(x_ptr + 1); - s10 = _mm256_srli_epi64(s8, 32); - s11 = _mm256_srli_epi64(s9, 32); - - s12 = _mm256_loadu_si256(y_ptr); - s13 = _mm256_loadu_si256(y_ptr + 1); - s14 = _mm256_srli_epi64(s12, 32); - s15 = _mm256_srli_epi64(s13, 32); - - s12 = _mm256_mul_epu32(s8, s12); // -> s0,s1 - s13 = _mm256_mul_epu32(s9, s13); // -> s2,s3 - s14 = _mm256_mul_epu32(s10, s14); // -> s0,s1 - s15 = _mm256_mul_epu32(s11, s15); // -> s2,s3 - - s10 = _mm256_slli_epi64(s12, 32); // -> s0 - s11 = _mm256_slli_epi64(s13, 32); // -> s2 - s12 = _mm256_srli_epi64(s12, 32); // -> s1 - s13 = _mm256_srli_epi64(s13, 32); // -> s3 - s10 = _mm256_srli_epi64(s10, 32); // -> s0 - s11 = _mm256_srli_epi64(s11, 32); // -> s2 - - s0 = _mm256_add_epi64(s0, s10); - s1 = _mm256_add_epi64(s1, s12); - s2 = _mm256_add_epi64(s2, s11); - s3 = _mm256_add_epi64(s3, s13); - - s10 = _mm256_slli_epi64(s14, 32); // -> s0 - s11 = _mm256_slli_epi64(s15, 32); // -> s2 - s14 = _mm256_srli_epi64(s14, 32); // -> s1 - s15 = _mm256_srli_epi64(s15, 32); // -> s3 - s10 = _mm256_srli_epi64(s10, 32); // -> s0 - s11 = _mm256_srli_epi64(s11, 32); // -> s2 - - s0 = _mm256_add_epi64(s0, s10); - s1 = _mm256_add_epi64(s1, s14); - s2 = _mm256_add_epi64(s2, s11); - s3 = _mm256_add_epi64(s3, s15); - - // deal with the second column - // s8,s9 are still in place! - s10 = _mm256_srli_epi64(s8, 32); - s11 = _mm256_srli_epi64(s9, 32); - - s12 = _mm256_loadu_si256(y_ptr + 2); - s13 = _mm256_loadu_si256(y_ptr + 3); - s14 = _mm256_srli_epi64(s12, 32); - s15 = _mm256_srli_epi64(s13, 32); - - s12 = _mm256_mul_epu32(s8, s12); // -> s4,s5 - s13 = _mm256_mul_epu32(s9, s13); // -> s6,s7 - s14 = _mm256_mul_epu32(s10, s14); // -> s4,s5 - s15 = _mm256_mul_epu32(s11, s15); // -> s6,s7 - - s10 = _mm256_slli_epi64(s12, 32); // -> s4 - s11 = _mm256_slli_epi64(s13, 32); // -> s6 - s12 = _mm256_srli_epi64(s12, 32); // -> s5 - s13 = _mm256_srli_epi64(s13, 32); // -> s7 - s10 = _mm256_srli_epi64(s10, 32); // -> s4 - s11 = _mm256_srli_epi64(s11, 32); // -> s6 - - s4 = _mm256_add_epi64(s4, s10); - s5 = _mm256_add_epi64(s5, s12); - s6 = _mm256_add_epi64(s6, s11); - s7 = _mm256_add_epi64(s7, s13); - - s10 = _mm256_slli_epi64(s14, 32); // -> s4 - s11 = _mm256_slli_epi64(s15, 32); // -> s6 - s14 = _mm256_srli_epi64(s14, 32); // -> s5 - s15 = _mm256_srli_epi64(s15, 32); // -> s7 - s10 = _mm256_srli_epi64(s10, 32); // -> s4 - s11 = _mm256_srli_epi64(s11, 32); // -> s6 - - s4 = _mm256_add_epi64(s4, s10); - s5 = _mm256_add_epi64(s5, s14); - s6 = _mm256_add_epi64(s6, s11); - s7 = _mm256_add_epi64(s7, s15); - - x_ptr += 2; - y_ptr += 4; - } - // final reduction - const uint64_t H2 = precomp->h; - s8 = _mm256_set1_epi64x((UINT64_C(1) << H2) - 1); // MASK2 - s9 = _mm256_loadu_si256((__m256i*)precomp->s2l_pow_red); // S2L_POW_RED - s10 = _mm256_loadu_si256((__m256i*)precomp->s2h_pow_red); // S2H_POW_RED - //--- s0,s1 - s11 = _mm256_and_si256(s1, s8); - s12 = _mm256_srli_epi64(s1, H2); - s13 = _mm256_mul_epu32(s11, s9); - s14 = _mm256_mul_epu32(s12, s10); - s0 = _mm256_add_epi64(s0, s13); - s0 = _mm256_add_epi64(s0, s14); - _mm256_storeu_si256(res_ptr + 0, s0); - //--- s2,s3 - s11 = _mm256_and_si256(s3, s8); - s12 = _mm256_srli_epi64(s3, H2); - s13 = _mm256_mul_epu32(s11, s9); - s14 = _mm256_mul_epu32(s12, s10); - s2 = _mm256_add_epi64(s2, s13); - s2 = _mm256_add_epi64(s2, s14); - _mm256_storeu_si256(res_ptr + 1, s2); - //--- s4,s5 - s11 = _mm256_and_si256(s5, s8); - s12 = _mm256_srli_epi64(s5, H2); - s13 = _mm256_mul_epu32(s11, s9); - s14 = _mm256_mul_epu32(s12, s10); - s4 = _mm256_add_epi64(s4, s13); - s4 = _mm256_add_epi64(s4, s14); - _mm256_storeu_si256(res_ptr + 2, s4); - //--- s6,s7 - s11 = _mm256_and_si256(s7, s8); - s12 = _mm256_srli_epi64(s7, H2); - s13 = _mm256_mul_epu32(s11, s9); - s14 = _mm256_mul_epu32(s12, s10); - s6 = _mm256_add_epi64(s6, s13); - s6 = _mm256_add_epi64(s6, s14); - _mm256_storeu_si256(res_ptr + 3, s6); -} - -EXPORT void q120x2_vec_mat2cols_product_bbc_avx2(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y) { - __m256i s0 = _mm256_setzero_si256(); // col 1a - __m256i s1 = _mm256_setzero_si256(); - __m256i s2 = _mm256_setzero_si256(); // col 1b - __m256i s3 = _mm256_setzero_si256(); - __m256i s4 = _mm256_setzero_si256(); // col 2a - __m256i s5 = _mm256_setzero_si256(); - __m256i s6 = _mm256_setzero_si256(); // col 2b - __m256i s7 = _mm256_setzero_si256(); - __m256i s8, s9, s10, s11; - __m256i s12, s13, s14, s15; - - s11 = _mm256_set1_epi64x(0xFFFFFFFFUL); - const __m256i* x_ptr = (__m256i*)x; - const __m256i* y_ptr = (__m256i*)y; - __m256i* res_ptr = (__m256i*)res; - for (uint64_t i = 0; i < ell; ++i) { - // treat item a - s8 = _mm256_loadu_si256(x_ptr); - s9 = _mm256_srli_epi64(s8, 32); - - s12 = _mm256_loadu_si256(y_ptr); - s13 = _mm256_loadu_si256(y_ptr + 2); - s14 = _mm256_srli_epi64(s12, 32); - s15 = _mm256_srli_epi64(s13, 32); - - s12 = _mm256_mul_epu32(s8, s12); // c1a -> s0,s1 - s13 = _mm256_mul_epu32(s8, s13); // c2a -> s4,s5 - s14 = _mm256_mul_epu32(s9, s14); // c1a -> s0,s1 - s15 = _mm256_mul_epu32(s9, s15); // c2a -> s4,s5 - - s8 = _mm256_and_si256(s12, s11); // -> s0 - s9 = _mm256_and_si256(s13, s11); // -> s4 - s12 = _mm256_srli_epi64(s12, 32); // -> s1 - s13 = _mm256_srli_epi64(s13, 32); // -> s5 - s0 = _mm256_add_epi64(s0, s8); - s1 = _mm256_add_epi64(s1, s12); - s4 = _mm256_add_epi64(s4, s9); - s5 = _mm256_add_epi64(s5, s13); - - s8 = _mm256_and_si256(s14, s11); // -> s0 - s9 = _mm256_and_si256(s15, s11); // -> s4 - s14 = _mm256_srli_epi64(s14, 32); // -> s1 - s15 = _mm256_srli_epi64(s15, 32); // -> s5 - s0 = _mm256_add_epi64(s0, s8); - s1 = _mm256_add_epi64(s1, s14); - s4 = _mm256_add_epi64(s4, s9); - s5 = _mm256_add_epi64(s5, s15); - - // treat item b - s8 = _mm256_loadu_si256(x_ptr + 1); - s9 = _mm256_srli_epi64(s8, 32); - - s12 = _mm256_loadu_si256(y_ptr + 1); - s13 = _mm256_loadu_si256(y_ptr + 3); - s14 = _mm256_srli_epi64(s12, 32); - s15 = _mm256_srli_epi64(s13, 32); - - s12 = _mm256_mul_epu32(s8, s12); // c1b -> s2,s3 - s13 = _mm256_mul_epu32(s8, s13); // c2b -> s6,s7 - s14 = _mm256_mul_epu32(s9, s14); // c1b -> s2,s3 - s15 = _mm256_mul_epu32(s9, s15); // c2b -> s6,s7 - - s8 = _mm256_and_si256(s12, s11); // -> s2 - s9 = _mm256_and_si256(s13, s11); // -> s6 - s12 = _mm256_srli_epi64(s12, 32); // -> s3 - s13 = _mm256_srli_epi64(s13, 32); // -> s7 - s2 = _mm256_add_epi64(s2, s8); - s3 = _mm256_add_epi64(s3, s12); - s6 = _mm256_add_epi64(s6, s9); - s7 = _mm256_add_epi64(s7, s13); - - s8 = _mm256_and_si256(s14, s11); // -> s2 - s9 = _mm256_and_si256(s15, s11); // -> s6 - s14 = _mm256_srli_epi64(s14, 32); // -> s3 - s15 = _mm256_srli_epi64(s15, 32); // -> s7 - s2 = _mm256_add_epi64(s2, s8); - s3 = _mm256_add_epi64(s3, s14); - s6 = _mm256_add_epi64(s6, s9); - s7 = _mm256_add_epi64(s7, s15); - - x_ptr += 2; - y_ptr += 4; - } - // final reduction - const uint64_t H2 = precomp->h; - s8 = _mm256_set1_epi64x((UINT64_C(1) << H2) - 1); // MASK2 - s9 = _mm256_loadu_si256((__m256i*)precomp->s2l_pow_red); // S2L_POW_RED - s10 = _mm256_loadu_si256((__m256i*)precomp->s2h_pow_red); // S2H_POW_RED - //--- s0,s1 - s11 = _mm256_and_si256(s1, s8); - s12 = _mm256_srli_epi64(s1, H2); - s13 = _mm256_mul_epu32(s11, s9); - s14 = _mm256_mul_epu32(s12, s10); - s0 = _mm256_add_epi64(s0, s13); - s0 = _mm256_add_epi64(s0, s14); - _mm256_storeu_si256(res_ptr + 0, s0); - //--- s2,s3 - s11 = _mm256_and_si256(s3, s8); - s12 = _mm256_srli_epi64(s3, H2); - s13 = _mm256_mul_epu32(s11, s9); - s14 = _mm256_mul_epu32(s12, s10); - s2 = _mm256_add_epi64(s2, s13); - s2 = _mm256_add_epi64(s2, s14); - _mm256_storeu_si256(res_ptr + 1, s2); - //--- s4,s5 - s11 = _mm256_and_si256(s5, s8); - s12 = _mm256_srli_epi64(s5, H2); - s13 = _mm256_mul_epu32(s11, s9); - s14 = _mm256_mul_epu32(s12, s10); - s4 = _mm256_add_epi64(s4, s13); - s4 = _mm256_add_epi64(s4, s14); - _mm256_storeu_si256(res_ptr + 2, s4); - //--- s6,s7 - s11 = _mm256_and_si256(s7, s8); - s12 = _mm256_srli_epi64(s7, H2); - s13 = _mm256_mul_epu32(s11, s9); - s14 = _mm256_mul_epu32(s12, s10); - s6 = _mm256_add_epi64(s6, s13); - s6 = _mm256_add_epi64(s6, s14); - _mm256_storeu_si256(res_ptr + 3, s6); -} - -EXPORT void q120x2_vec_mat1col_product_bbc_avx2(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y) { - __m256i s0 = _mm256_setzero_si256(); // col 1a - __m256i s1 = _mm256_setzero_si256(); - __m256i s2 = _mm256_setzero_si256(); // col 1b - __m256i s3 = _mm256_setzero_si256(); - __m256i s4 = _mm256_set1_epi64x(0xFFFFFFFFUL); - __m256i s8, s9, s10, s11; - __m256i s12, s13, s14, s15; - - const __m256i* x_ptr = (__m256i*)x; - const __m256i* y_ptr = (__m256i*)y; - __m256i* res_ptr = (__m256i*)res; - for (uint64_t i = 0; i < ell; ++i) { - s8 = _mm256_loadu_si256(x_ptr); - s9 = _mm256_loadu_si256(x_ptr + 1); - s10 = _mm256_srli_epi64(s8, 32); - s11 = _mm256_srli_epi64(s9, 32); - - s12 = _mm256_loadu_si256(y_ptr); - s13 = _mm256_loadu_si256(y_ptr + 1); - s14 = _mm256_srli_epi64(s12, 32); - s15 = _mm256_srli_epi64(s13, 32); - - s12 = _mm256_mul_epu32(s8, s12); // -> s0,s1 - s13 = _mm256_mul_epu32(s9, s13); // -> s2,s3 - s14 = _mm256_mul_epu32(s10, s14); // -> s0,s1 - s15 = _mm256_mul_epu32(s11, s15); // -> s2,s3 - - s8 = _mm256_and_si256(s12, s4); // -> s0 - s9 = _mm256_and_si256(s13, s4); // -> s2 - s10 = _mm256_and_si256(s14, s4); // -> s0 - s11 = _mm256_and_si256(s15, s4); // -> s2 - s12 = _mm256_srli_epi64(s12, 32); // -> s1 - s13 = _mm256_srli_epi64(s13, 32); // -> s3 - s14 = _mm256_srli_epi64(s14, 32); // -> s1 - s15 = _mm256_srli_epi64(s15, 32); // -> s3 - - s0 = _mm256_add_epi64(s0, s8); - s1 = _mm256_add_epi64(s1, s12); - s2 = _mm256_add_epi64(s2, s9); - s3 = _mm256_add_epi64(s3, s13); - s0 = _mm256_add_epi64(s0, s10); - s1 = _mm256_add_epi64(s1, s14); - s2 = _mm256_add_epi64(s2, s11); - s3 = _mm256_add_epi64(s3, s15); - - x_ptr += 2; - y_ptr += 2; - } - // final reduction - const uint64_t H2 = precomp->h; - s8 = _mm256_set1_epi64x((UINT64_C(1) << H2) - 1); // MASK2 - s9 = _mm256_loadu_si256((__m256i*)precomp->s2l_pow_red); // S2L_POW_RED - s10 = _mm256_loadu_si256((__m256i*)precomp->s2h_pow_red); // S2H_POW_RED - //--- s0,s1 - s11 = _mm256_and_si256(s1, s8); - s12 = _mm256_srli_epi64(s1, H2); - s13 = _mm256_mul_epu32(s11, s9); - s14 = _mm256_mul_epu32(s12, s10); - s0 = _mm256_add_epi64(s0, s13); - s0 = _mm256_add_epi64(s0, s14); - _mm256_storeu_si256(res_ptr + 0, s0); - //--- s2,s3 - s11 = _mm256_and_si256(s3, s8); - s12 = _mm256_srli_epi64(s3, H2); - s13 = _mm256_mul_epu32(s11, s9); - s14 = _mm256_mul_epu32(s12, s10); - s2 = _mm256_add_epi64(s2, s13); - s2 = _mm256_add_epi64(s2, s14); - _mm256_storeu_si256(res_ptr + 1, s2); -}
D spqlios/lib/spqlios/q120/q120_arithmetic_private.h

@@ -1,37 +0,0 @@

-#ifndef SPQLIOS_Q120_ARITHMETIC_DEF_H -#define SPQLIOS_Q120_ARITHMETIC_DEF_H - -#include <stdint.h> - -typedef struct _q120_mat1col_product_baa_precomp { - uint64_t h; - uint64_t h_pow_red[4]; -#ifndef NDEBUG - double res_bit_size; -#endif -} q120_mat1col_product_baa_precomp; - -typedef struct _q120_mat1col_product_bbb_precomp { - uint64_t h; - uint64_t s1h_pow_red[4]; - uint64_t s2l_pow_red[4]; - uint64_t s2h_pow_red[4]; - uint64_t s3l_pow_red[4]; - uint64_t s3h_pow_red[4]; - uint64_t s4l_pow_red[4]; - uint64_t s4h_pow_red[4]; -#ifndef NDEBUG - double res_bit_size; -#endif -} q120_mat1col_product_bbb_precomp; - -typedef struct _q120_mat1col_product_bbc_precomp { - uint64_t h; - uint64_t s2l_pow_red[4]; - uint64_t s2h_pow_red[4]; -#ifndef NDEBUG - double res_bit_size; -#endif -} q120_mat1col_product_bbc_precomp; - -#endif // SPQLIOS_Q120_ARITHMETIC_DEF_H
D spqlios/lib/spqlios/q120/q120_arithmetic_ref.c

@@ -1,506 +0,0 @@

-#include <assert.h> -#include <math.h> -#include <stdint.h> - -#include "q120_arithmetic.h" -#include "q120_arithmetic_private.h" -#include "q120_common.h" - -#define MODQ(val, q) ((val) % (q)) - -double comp_bit_size_red(const uint64_t h, const uint64_t qs[4]) { - assert(h < 128); - double h_pow2_bs = 0; - for (uint64_t k = 0; k < 4; ++k) { - double t = log2((double)MODQ((__uint128_t)1 << h, qs[k])); - if (t > h_pow2_bs) h_pow2_bs = t; - } - return h_pow2_bs; -} - -double comp_bit_size_sum(const uint64_t n, const double* const bs) { - double s = 0; - for (uint64_t i = 0; i < n; ++i) { - s += pow(2, bs[i]); - } - return log2(s); -} - -void vec_mat1col_product_baa_precomp(q120_mat1col_product_baa_precomp* precomp) { - uint64_t qs[4] = {Q1, Q2, Q3, Q4}; - - double min_res_bs = 1000; - uint64_t min_h = -1; - - double ell_bs = log2((double)MAX_ELL); - for (uint64_t h = 1; h < 64; ++h) { - double h_pow2_bs = comp_bit_size_red(h, qs); - - const double bs[] = {h + ell_bs, 64 - h + ell_bs + h_pow2_bs}; - const double res_bs = comp_bit_size_sum(2, bs); - - if (min_res_bs > res_bs) { - min_res_bs = res_bs; - min_h = h; - } - } - - assert(min_res_bs < 64); - precomp->h = min_h; - for (uint64_t k = 0; k < 4; ++k) { - precomp->h_pow_red[k] = MODQ(UINT64_C(1) << precomp->h, qs[k]); - } -#ifndef NDEBUG - precomp->res_bit_size = min_res_bs; -#endif - // printf("AA %lu %lf\n", min_h, min_res_bs); -} - -EXPORT q120_mat1col_product_baa_precomp* q120_new_vec_mat1col_product_baa_precomp() { - q120_mat1col_product_baa_precomp* res = malloc(sizeof(q120_mat1col_product_baa_precomp)); - vec_mat1col_product_baa_precomp(res); - return res; -} - -EXPORT void q120_delete_vec_mat1col_product_baa_precomp(q120_mat1col_product_baa_precomp* addr) { free(addr); } - -EXPORT void q120_vec_mat1col_product_baa_ref(q120_mat1col_product_baa_precomp* precomp, const uint64_t ell, - q120b* const res, const q120a* const x, const q120a* const y) { - /** - * Algorithm: - * - res = acc1 + acc2 . ((2^H) % Q) - * - acc1 is the sum of H LSB of products x[i].y[i] - * - acc2 is the sum of 64-H MSB of products x[i]].y[i] - * - for l < 10k acc1 will have H + log2(10000) and acc2 64 - H + log2(10000) bits - * - final sum has max(H, 64 - H + bit_size((2^H) % Q)) + log2(10000) + 1 bits - */ - const uint64_t H = precomp->h; - const uint64_t MASK = (UINT64_C(1) << H) - 1; - - uint64_t acc1[4] = {0, 0, 0, 0}; // accumulate H least significant bits of product - uint64_t acc2[4] = {0, 0, 0, 0}; // accumulate 64 - H most significan bits of product - - const uint64_t* const x_ptr = (uint64_t*)x; - const uint64_t* const y_ptr = (uint64_t*)y; - - for (uint64_t i = 0; i < 4 * ell; i += 4) { - for (uint64_t j = 0; j < 4; ++j) { - uint64_t t = x_ptr[i + j] * y_ptr[i + j]; - acc1[j] += t & MASK; - acc2[j] += t >> H; - } - } - - uint64_t* const res_ptr = (uint64_t*)res; - for (uint64_t j = 0; j < 4; ++j) { - res_ptr[j] = acc1[j] + acc2[j] * precomp->h_pow_red[j]; - assert(log2(res_ptr[j]) < precomp->res_bit_size); - } -} - -void vec_mat1col_product_bbb_precomp(q120_mat1col_product_bbb_precomp* precomp) { - uint64_t qs[4] = {Q1, Q2, Q3, Q4}; - - double ell_bs = log2((double)MAX_ELL); - double min_res_bs = 1000; - uint64_t min_h = -1; - - const double s1_bs = 32 + ell_bs; - const double s2_bs = 32 + ell_bs + log2(3); - const double s3_bs = 32 + ell_bs + log2(3); - const double s4_bs = 32 + ell_bs; - for (uint64_t h = 16; h < 32; ++h) { - const double s1l_bs = h; - const double s1h_bs = (s1_bs - h) + comp_bit_size_red(h, qs); - const double s2l_bs = h + comp_bit_size_red(32, qs); - const double s2h_bs = (s2_bs - h) + comp_bit_size_red(32 + h, qs); - const double s3l_bs = h + comp_bit_size_red(64, qs); - const double s3h_bs = (s3_bs - h) + comp_bit_size_red(64 + h, qs); - const double s4l_bs = h + comp_bit_size_red(96, qs); - const double s4h_bs = (s4_bs - h) + comp_bit_size_red(96 + h, qs); - - const double bs[] = {s1l_bs, s1h_bs, s2l_bs, s2h_bs, s3l_bs, s3h_bs, s4l_bs, s4h_bs}; - const double res_bs = comp_bit_size_sum(8, bs); - - if (min_res_bs > res_bs) { - min_res_bs = res_bs; - min_h = h; - } - } - - assert(min_res_bs < 64); - precomp->h = min_h; - for (uint64_t k = 0; k < 4; ++k) { - precomp->s1h_pow_red[k] = UINT64_C(1) << precomp->h; // 2^24 - precomp->s2l_pow_red[k] = MODQ(UINT64_C(1) << 32, qs[k]); // 2^32 - precomp->s2h_pow_red[k] = MODQ(precomp->s2l_pow_red[k] * precomp->s1h_pow_red[k], qs[k]); // 2^(32+24) - precomp->s3l_pow_red[k] = MODQ(precomp->s2l_pow_red[k] * precomp->s2l_pow_red[k], qs[k]); // 2^64 = 2^(32+32) - precomp->s3h_pow_red[k] = MODQ(precomp->s3l_pow_red[k] * precomp->s1h_pow_red[k], qs[k]); // 2^(64+24) - precomp->s4l_pow_red[k] = MODQ(precomp->s3l_pow_red[k] * precomp->s2l_pow_red[k], qs[k]); // 2^96 = 2^(64+32) - precomp->s4h_pow_red[k] = MODQ(precomp->s4l_pow_red[k] * precomp->s1h_pow_red[k], qs[k]); // 2^(96+24) - } -// printf("AA %lu %lf\n", min_h, min_res_bs); -#ifndef NDEBUG - precomp->res_bit_size = min_res_bs; -#endif -} - -EXPORT q120_mat1col_product_bbb_precomp* q120_new_vec_mat1col_product_bbb_precomp() { - q120_mat1col_product_bbb_precomp* res = malloc(sizeof(q120_mat1col_product_bbb_precomp)); - vec_mat1col_product_bbb_precomp(res); - return res; -} - -EXPORT void q120_delete_vec_mat1col_product_bbb_precomp(q120_mat1col_product_bbb_precomp* addr) { free(addr); } - -EXPORT void q120_vec_mat1col_product_bbb_ref(q120_mat1col_product_bbb_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120b* const y) { - /** - * Algorithm: - * 1. Split x_i and y_i in 2 32-bit parts and compute the cross-products: - * - x_i = xl_i + xh_i . 2^32 - * - y_i = yl_i + yh_i . 2^32 - * - A_i = xl_i . yl_i - * - B_i = xl_i . yh_i - * - C_i = xh_i . yl_i - * - D_i = xh_i . yh_i - * - we have x_i . y_i == A_i + (B_i + C_i) . 2^32 + D_i . 2^64 - * 2. Split A_i, B_i, C_i and D_i into 2 32-bit parts - * - A_i = Al_i + Ah_i . 2^32 - * - B_i = Bl_i + Bh_i . 2^32 - * - C_i = Cl_i + Ch_i . 2^32 - * - D_i = Dl_i + Dh_i . 2^32 - * 3. Compute the sums: - * - S1 = \sum Al_i - * - S2 = \sum (Ah_i + Bl_i + Cl_i) - * - S3 = \sum (Bh_i + Ch_i + Dl_i) - * - S4 = \sum Dh_i - * - here S1, S4 have 32 + log2(ell) bits and S2, S3 have 32 + log2(ell) + - * log2(3) bits - * - for ell == 10000 S2, S3 have < 47 bits - * 4. Split S1, S2, S3 and S4 in 2 24-bit parts (24 = ceil(47/2)) - * - S1 = S1l + S1h . 2^24 - * - S2 = S2l + S2h . 2^24 - * - S3 = S3l + S3h . 2^24 - * - S4 = S4l + S4h . 2^24 - * 5. Compute final result as: - * - \sum x_i . y_i = S1l + S1h . 2^24 - * + S2l . 2^32 + S2h . 2^(32+24) - * + S3l . 2^64 + S3h . 2^(64 + 24) - * + S4l . 2^96 + S4l . 2^(96+24) - * - here the powers of 2 are reduced modulo the primes Q before - * multiplications - * - the result will be on 24 + 3 + bit size of primes Q - */ - const uint64_t H1 = 32; - const uint64_t MASK1 = (UINT64_C(1) << H1) - 1; - - uint64_t s1[4] = {0, 0, 0, 0}; - uint64_t s2[4] = {0, 0, 0, 0}; - uint64_t s3[4] = {0, 0, 0, 0}; - uint64_t s4[4] = {0, 0, 0, 0}; - - const uint64_t* const x_ptr = (uint64_t*)x; - const uint64_t* const y_ptr = (uint64_t*)y; - - for (uint64_t i = 0; i < 4 * ell; i += 4) { - for (uint64_t j = 0; j < 4; ++j) { - const uint64_t xl = x_ptr[i + j] & MASK1; - const uint64_t xh = x_ptr[i + j] >> H1; - const uint64_t yl = y_ptr[i + j] & MASK1; - const uint64_t yh = y_ptr[i + j] >> H1; - - const uint64_t a = xl * yl; - const uint64_t al = a & MASK1; - const uint64_t ah = a >> H1; - - const uint64_t b = xl * yh; - const uint64_t bl = b & MASK1; - const uint64_t bh = b >> H1; - - const uint64_t c = xh * yl; - const uint64_t cl = c & MASK1; - const uint64_t ch = c >> H1; - - const uint64_t d = xh * yh; - const uint64_t dl = d & MASK1; - const uint64_t dh = d >> H1; - - s1[j] += al; - s2[j] += ah + bl + cl; - s3[j] += bh + ch + dl; - s4[j] += dh; - } - } - - const uint64_t H2 = precomp->h; - const uint64_t MASK2 = (UINT64_C(1) << H2) - 1; - - uint64_t* const res_ptr = (uint64_t*)res; - for (uint64_t j = 0; j < 4; ++j) { - const uint64_t s1l = s1[j] & MASK2; - const uint64_t s1h = s1[j] >> H2; - const uint64_t s2l = s2[j] & MASK2; - const uint64_t s2h = s2[j] >> H2; - const uint64_t s3l = s3[j] & MASK2; - const uint64_t s3h = s3[j] >> H2; - const uint64_t s4l = s4[j] & MASK2; - const uint64_t s4h = s4[j] >> H2; - - uint64_t t = s1l; - t += s1h * precomp->s1h_pow_red[j]; - t += s2l * precomp->s2l_pow_red[j]; - t += s2h * precomp->s2h_pow_red[j]; - t += s3l * precomp->s3l_pow_red[j]; - t += s3h * precomp->s3h_pow_red[j]; - t += s4l * precomp->s4l_pow_red[j]; - t += s4h * precomp->s4h_pow_red[j]; - - res_ptr[j] = t; - assert(log2(res_ptr[j]) < precomp->res_bit_size); - } -} - -void vec_mat1col_product_bbc_precomp(q120_mat1col_product_bbc_precomp* precomp) { - uint64_t qs[4] = {Q1, Q2, Q3, Q4}; - - double min_res_bs = 1000; - uint64_t min_h = -1; - - double pow2_32_bs = comp_bit_size_red(32, qs); - - double ell_bs = log2((double)MAX_ELL); - double s1_bs = 32 + ell_bs; - for (uint64_t h = 16; h < 32; ++h) { - double s2l_bs = pow2_32_bs + h; - double s2h_bs = s1_bs - h + comp_bit_size_red(32 + h, qs); - - const double bs[] = {s1_bs, s2l_bs, s2h_bs}; - const double res_bs = comp_bit_size_sum(3, bs); - - if (min_res_bs > res_bs) { - min_res_bs = res_bs; - min_h = h; - } - } - - assert(min_res_bs < 64); - precomp->h = min_h; - for (uint64_t k = 0; k < 4; ++k) { - precomp->s2l_pow_red[k] = MODQ(UINT64_C(1) << 32, qs[k]); - precomp->s2h_pow_red[k] = MODQ(UINT64_C(1) << (32 + precomp->h), qs[k]); - } -#ifndef NDEBUG - precomp->res_bit_size = min_res_bs; -#endif - // printf("AA %lu %lf\n", min_h, min_res_bs); -} - -EXPORT q120_mat1col_product_bbc_precomp* q120_new_vec_mat1col_product_bbc_precomp() { - q120_mat1col_product_bbc_precomp* res = malloc(sizeof(q120_mat1col_product_bbc_precomp)); - vec_mat1col_product_bbc_precomp(res); - return res; -} - -EXPORT void q120_delete_vec_mat1col_product_bbc_precomp(q120_mat1col_product_bbc_precomp* addr) { free(addr); } - -EXPORT void q120_vec_mat1col_product_bbc_ref_old(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y) { - /** - * Algorithm: - * 0. We have - * - y0_i == y_i % Q and y1_i == (y_i . 2^32) % Q - * 1. Split x_i in 2 32-bit parts and compute the cross-products: - * - x_i = xl_i + xh_i . 2^32 - * - A_i = xl_i . y1_i - * - B_i = xh_i . y2_i - * - we have x_i . y_i == A_i + B_i - * 2. Split A_i and B_i into 2 32-bit parts - * - A_i = Al_i + Ah_i . 2^32 - * - B_i = Bl_i + Bh_i . 2^32 - * 3. Compute the sums: - * - S1 = \sum Al_i + Bl_i - * - S2 = \sum Ah_i + Bh_i - * - here S1 and S2 have 32 + log2(ell) bits - * - for ell == 10000 S1, S2 have < 46 bits - * 4. Split S2 in 27-bit and 19-bit parts (27+19 == 46) - * - S2 = S2l + S2h . 2^27 - * 5. Compute final result as: - * - \sum x_i . y_i = S1 + S2l . 2^32 + S2h . 2^(32+27) - * - here the powers of 2 are reduced modulo the primes Q before - * multiplications - * - the result will be on < 52 bits - */ - - const uint64_t H1 = 32; - const uint64_t MASK1 = (UINT64_C(1) << H1) - 1; - - uint64_t s1[4] = {0, 0, 0, 0}; - uint64_t s2[4] = {0, 0, 0, 0}; - - const uint64_t* const x_ptr = (uint64_t*)x; - const uint32_t* const y_ptr = (uint32_t*)y; - - for (uint64_t i = 0; i < 4 * ell; i += 4) { - for (uint64_t j = 0; j < 4; ++j) { - const uint64_t xl = x_ptr[i + j] & MASK1; - const uint64_t xh = x_ptr[i + j] >> H1; - const uint64_t y0 = y_ptr[2 * (i + j)]; - const uint64_t y1 = y_ptr[2 * (i + j) + 1]; - - const uint64_t a = xl * y0; - const uint64_t al = a & MASK1; - const uint64_t ah = a >> H1; - - const uint64_t b = xh * y1; - const uint64_t bl = b & MASK1; - const uint64_t bh = b >> H1; - - s1[j] += al + bl; - s2[j] += ah + bh; - } - } - - const uint64_t H2 = precomp->h; - const uint64_t MASK2 = (UINT64_C(1) << H2) - 1; - - uint64_t* const res_ptr = (uint64_t*)res; - for (uint64_t k = 0; k < 4; ++k) { - const uint64_t s2l = s2[k] & MASK2; - const uint64_t s2h = s2[k] >> H2; - - uint64_t t = s1[k]; - t += s2l * precomp->s2l_pow_red[k]; - t += s2h * precomp->s2h_pow_red[k]; - - res_ptr[k] = t; - assert(log2(res_ptr[k]) < precomp->res_bit_size); - } -} - -static __always_inline void accum_mul_q120_bc(uint64_t res[8], // - const uint32_t x_layb[8], const uint32_t y_layc[8]) { - for (uint64_t i = 0; i < 4; ++i) { - static const uint64_t MASK32 = 0xFFFFFFFFUL; - uint64_t x_lo = x_layb[2 * i]; - uint64_t x_hi = x_layb[2 * i + 1]; - uint64_t y_lo = y_layc[2 * i]; - uint64_t y_hi = y_layc[2 * i + 1]; - uint64_t xy_lo = x_lo * y_lo; - uint64_t xy_hi = x_hi * y_hi; - res[2 * i] += (xy_lo & MASK32) + (xy_hi & MASK32); - res[2 * i + 1] += (xy_lo >> 32) + (xy_hi >> 32); - } -} - -static __always_inline void accum_to_q120b(uint64_t res[4], // - const uint64_t s[8], const q120_mat1col_product_bbc_precomp* precomp) { - const uint64_t H2 = precomp->h; - const uint64_t MASK2 = (UINT64_C(1) << H2) - 1; - for (uint64_t k = 0; k < 4; ++k) { - const uint64_t s2l = s[2 * k + 1] & MASK2; - const uint64_t s2h = s[2 * k + 1] >> H2; - uint64_t t = s[2 * k]; - t += s2l * precomp->s2l_pow_red[k]; - t += s2h * precomp->s2h_pow_red[k]; - res[k] = t; - assert(log2(res[k]) < precomp->res_bit_size); - } -} - -EXPORT void q120_vec_mat1col_product_bbc_ref(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y) { - uint64_t s[8] = {0, 0, 0, 0, 0, 0, 0, 0}; - - const uint32_t(*const x_ptr)[8] = (const uint32_t(*const)[8])x; - const uint32_t(*const y_ptr)[8] = (const uint32_t(*const)[8])y; - - for (uint64_t i = 0; i < ell; i++) { - accum_mul_q120_bc(s, x_ptr[i], y_ptr[i]); - } - accum_to_q120b((uint64_t*)res, s, precomp); -} - -EXPORT void q120x2_vec_mat1col_product_bbc_ref(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y) { - uint64_t s[2][16] = {0}; - - const uint32_t(*const x_ptr)[2][8] = (const uint32_t(*const)[2][8])x; - const uint32_t(*const y_ptr)[2][8] = (const uint32_t(*const)[2][8])y; - uint64_t(*re)[4] = (uint64_t(*)[4])res; - - for (uint64_t i = 0; i < ell; i++) { - accum_mul_q120_bc(s[0], x_ptr[i][0], y_ptr[i][0]); - accum_mul_q120_bc(s[1], x_ptr[i][1], y_ptr[i][1]); - } - accum_to_q120b(re[0], s[0], precomp); - accum_to_q120b(re[1], s[1], precomp); -} - -EXPORT void q120x2_vec_mat2cols_product_bbc_ref(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y) { - uint64_t s[4][16] = {0}; - - const uint32_t(*const x_ptr)[2][8] = (const uint32_t(*const)[2][8])x; - const uint32_t(*const y_ptr)[4][8] = (const uint32_t(*const)[4][8])y; - uint64_t(*re)[4] = (uint64_t(*)[4])res; - - for (uint64_t i = 0; i < ell; i++) { - accum_mul_q120_bc(s[0], x_ptr[i][0], y_ptr[i][0]); - accum_mul_q120_bc(s[1], x_ptr[i][1], y_ptr[i][1]); - accum_mul_q120_bc(s[2], x_ptr[i][0], y_ptr[i][2]); - accum_mul_q120_bc(s[3], x_ptr[i][1], y_ptr[i][3]); - } - accum_to_q120b(re[0], s[0], precomp); - accum_to_q120b(re[1], s[1], precomp); - accum_to_q120b(re[2], s[2], precomp); - accum_to_q120b(re[3], s[3], precomp); -} - -EXPORT void q120x2_extract_1blk_from_q120b_ref(uint64_t nn, uint64_t blk, - q120x2b* const dst, // 8 doubles - const q120b* const src // a q120b vector -) { - const uint64_t* in = (uint64_t*)src; - uint64_t* out = (uint64_t*)dst; - for (uint64_t i = 0; i < 8; ++i) { - out[i] = in[8 * blk + i]; - } -} - -// function on layout c is the exact same as on layout b -#ifdef __APPLE__ -#pragma weak q120x2_extract_1blk_from_q120c_ref = q120x2_extract_1blk_from_q120b_ref -#else -EXPORT void q120x2_extract_1blk_from_q120c_ref(uint64_t nn, uint64_t blk, - q120x2c* const dst, // 8 doubles - const q120c* const src // a q120c vector - ) __attribute__((alias("q120x2_extract_1blk_from_q120b_ref"))); -#endif - -EXPORT void q120x2_extract_1blk_from_contiguous_q120b_ref( - uint64_t nn, uint64_t nrows, uint64_t blk, - q120x2b* const dst, // nrows * 2 q120 - const q120b* const src // a contiguous array of nrows q120b vectors -) { - const uint64_t* in = (uint64_t*)src; - uint64_t* out = (uint64_t*)dst; - for (uint64_t row = 0; row < nrows; ++row) { - for (uint64_t i = 0; i < 8; ++i) { - out[i] = in[8 * blk + i]; - } - in += 4 * nn; - out += 8; - } -} - -EXPORT void q120x2b_save_1blk_to_q120b_ref(uint64_t nn, uint64_t blk, - q120b* dest, // 1 reim vector of length m - const q120x2b* src // 8 doubles -) { - const uint64_t* in = (uint64_t*)src; - uint64_t* out = (uint64_t*)dest; - for (uint64_t i = 0; i < 8; ++i) { - out[8 * blk + i] = in[i]; - } -}
D spqlios/lib/spqlios/q120/q120_arithmetic_simple.c

@@ -1,111 +0,0 @@

-#include <assert.h> -#include <math.h> -#include <stdint.h> - -#include "q120_arithmetic.h" -#include "q120_common.h" - -EXPORT void q120_add_bbb_simple(uint64_t nn, q120b* const res, const q120b* const x, const q120b* const y) { - const uint64_t* x_u64 = (uint64_t*)x; - const uint64_t* y_u64 = (uint64_t*)y; - uint64_t* res_u64 = (uint64_t*)res; - for (uint64_t i = 0; i < 4 * nn; i += 4) { - res_u64[i + 0] = x_u64[i + 0] % ((uint64_t)Q1 << 33) + y_u64[i + 0] % ((uint64_t)Q1 << 33); - res_u64[i + 1] = x_u64[i + 1] % ((uint64_t)Q2 << 33) + y_u64[i + 1] % ((uint64_t)Q2 << 33); - res_u64[i + 2] = x_u64[i + 2] % ((uint64_t)Q3 << 33) + y_u64[i + 2] % ((uint64_t)Q3 << 33); - res_u64[i + 3] = x_u64[i + 3] % ((uint64_t)Q4 << 33) + y_u64[i + 3] % ((uint64_t)Q4 << 33); - } -} - -EXPORT void q120_add_ccc_simple(uint64_t nn, q120c* const res, const q120c* const x, const q120c* const y) { - const uint32_t* x_u32 = (uint32_t*)x; - const uint32_t* y_u32 = (uint32_t*)y; - uint32_t* res_u32 = (uint32_t*)res; - for (uint64_t i = 0; i < 8 * nn; i += 8) { - res_u32[i + 0] = (uint32_t)(((uint64_t)x_u32[i + 0] + (uint64_t)y_u32[i + 0]) % Q1); - res_u32[i + 1] = (uint32_t)(((uint64_t)x_u32[i + 1] + (uint64_t)y_u32[i + 1]) % Q1); - res_u32[i + 2] = (uint32_t)(((uint64_t)x_u32[i + 2] + (uint64_t)y_u32[i + 2]) % Q2); - res_u32[i + 3] = (uint32_t)(((uint64_t)x_u32[i + 3] + (uint64_t)y_u32[i + 3]) % Q2); - res_u32[i + 4] = (uint32_t)(((uint64_t)x_u32[i + 4] + (uint64_t)y_u32[i + 4]) % Q3); - res_u32[i + 5] = (uint32_t)(((uint64_t)x_u32[i + 5] + (uint64_t)y_u32[i + 5]) % Q3); - res_u32[i + 6] = (uint32_t)(((uint64_t)x_u32[i + 6] + (uint64_t)y_u32[i + 6]) % Q4); - res_u32[i + 7] = (uint32_t)(((uint64_t)x_u32[i + 7] + (uint64_t)y_u32[i + 7]) % Q4); - } -} - -EXPORT void q120_c_from_b_simple(uint64_t nn, q120c* const res, const q120b* const x) { - const uint64_t* x_u64 = (uint64_t*)x; - uint32_t* res_u32 = (uint32_t*)res; - for (uint64_t i = 0, j = 0; i < 4 * nn; i += 4, j += 8) { - res_u32[j + 0] = x_u64[i + 0] % Q1; - res_u32[j + 1] = ((uint64_t)res_u32[j + 0] << 32) % Q1; - res_u32[j + 2] = x_u64[i + 1] % Q2; - res_u32[j + 3] = ((uint64_t)res_u32[j + 2] << 32) % Q2; - res_u32[j + 4] = x_u64[i + 2] % Q3; - res_u32[j + 5] = ((uint64_t)res_u32[j + 4] << 32) % Q3; - res_u32[j + 6] = x_u64[i + 3] % Q4; - res_u32[j + 7] = ((uint64_t)res_u32[j + 6] << 32) % Q4; - } -} - -EXPORT void q120_b_from_znx64_simple(uint64_t nn, q120b* const res, const int64_t* const x) { - static const int64_t MASK_HI = INT64_C(0x8000000000000000); - static const int64_t MASK_LO = ~MASK_HI; - static const uint64_t OQ[4] = { - (Q1 - (UINT64_C(0x8000000000000000) % Q1)), - (Q2 - (UINT64_C(0x8000000000000000) % Q2)), - (Q3 - (UINT64_C(0x8000000000000000) % Q3)), - (Q4 - (UINT64_C(0x8000000000000000) % Q4)), - }; - uint64_t* res_u64 = (uint64_t*)res; - for (uint64_t i = 0, j = 0; j < nn; i += 4, ++j) { - uint64_t xj_lo = x[j] & MASK_LO; - uint64_t xj_hi = x[j] & MASK_HI; - res_u64[i + 0] = xj_lo + (xj_hi ? OQ[0] : 0); - res_u64[i + 1] = xj_lo + (xj_hi ? OQ[1] : 0); - res_u64[i + 2] = xj_lo + (xj_hi ? OQ[2] : 0); - res_u64[i + 3] = xj_lo + (xj_hi ? OQ[3] : 0); - } -} - -static int64_t posmod(int64_t x, int64_t q) { - int64_t t = x % q; - if (t < 0) - return t + q; - else - return t; -} - -EXPORT void q120_c_from_znx64_simple(uint64_t nn, q120c* const res, const int64_t* const x) { - uint32_t* res_u32 = (uint32_t*)res; - for (uint64_t i = 0, j = 0; j < nn; i += 8, ++j) { - res_u32[i + 0] = posmod(x[j], Q1); - res_u32[i + 1] = ((uint64_t)res_u32[i + 0] << 32) % Q1; - res_u32[i + 2] = posmod(x[j], Q2); - res_u32[i + 3] = ((uint64_t)res_u32[i + 2] << 32) % Q2; - res_u32[i + 4] = posmod(x[j], Q3); - res_u32[i + 5] = ((uint64_t)res_u32[i + 4] << 32) % Q3; - res_u32[i + 6] = posmod(x[j], Q4); - res_u32[i + 7] = ((uint64_t)res_u32[i + 6] << 32) % Q4; - ; - } -} - -EXPORT void q120_b_to_znx128_simple(uint64_t nn, __int128_t* const res, const q120b* const x) { - static const __int128_t Q = (__int128_t)Q1 * Q2 * Q3 * Q4; - static const __int128_t Qm1 = (__int128_t)Q2 * Q3 * Q4; - static const __int128_t Qm2 = (__int128_t)Q1 * Q3 * Q4; - static const __int128_t Qm3 = (__int128_t)Q1 * Q2 * Q4; - static const __int128_t Qm4 = (__int128_t)Q1 * Q2 * Q3; - - const uint64_t* x_u64 = (uint64_t*)x; - for (uint64_t i = 0, j = 0; j < nn; i += 4, ++j) { - __int128_t tmp = 0; - tmp += (((x_u64[i + 0] % Q1) * Q1_CRT_CST) % Q1) * Qm1; - tmp += (((x_u64[i + 1] % Q2) * Q2_CRT_CST) % Q2) * Qm2; - tmp += (((x_u64[i + 2] % Q3) * Q3_CRT_CST) % Q3) * Qm3; - tmp += (((x_u64[i + 3] % Q4) * Q4_CRT_CST) % Q4) * Qm4; - tmp %= Q; - res[j] = (tmp >= (Q + 1) / 2) ? tmp - Q : tmp; - } -}
D spqlios/lib/spqlios/q120/q120_common.h

@@ -1,94 +0,0 @@

-#ifndef SPQLIOS_Q120_COMMON_H -#define SPQLIOS_Q120_COMMON_H - -#include <stdint.h> - -#if !defined(SPQLIOS_Q120_USE_29_BIT_PRIMES) && !defined(SPQLIOS_Q120_USE_30_BIT_PRIMES) && \ - !defined(SPQLIOS_Q120_USE_31_BIT_PRIMES) -#define SPQLIOS_Q120_USE_30_BIT_PRIMES -#endif - -/** - * 29-bit primes and 2*2^16 roots of unity - */ -#ifdef SPQLIOS_Q120_USE_29_BIT_PRIMES -#define Q1 ((1u << 29) - 2 * (1u << 17) + 1) -#define OMEGA1 78289835 -#define Q1_CRT_CST 301701286 // (Q2*Q3*Q4)^-1 mod Q1 - -#define Q2 ((1u << 29) - 5 * (1u << 17) + 1) -#define OMEGA2 178519192 -#define Q2_CRT_CST 536020447 // (Q1*Q3*Q4)^-1 mod Q2 - -#define Q3 ((1u << 29) - 26 * (1u << 17) + 1) -#define OMEGA3 483889678 -#define Q3_CRT_CST 86367873 // (Q1*Q2*Q4)^-1 mod Q3 - -#define Q4 ((1u << 29) - 35 * (1u << 17) + 1) -#define OMEGA4 239808033 -#define Q4_CRT_CST 147030781 // (Q1*Q2*Q3)^-1 mod Q4 -#endif - -/** - * 30-bit primes and 2*2^16 roots of unity - */ -#ifdef SPQLIOS_Q120_USE_30_BIT_PRIMES -#define Q1 ((1u << 30) - 2 * (1u << 17) + 1) -#define OMEGA1 1070907127 -#define Q1_CRT_CST 43599465 // (Q2*Q3*Q4)^-1 mod Q1 - -#define Q2 ((1u << 30) - 17 * (1u << 17) + 1) -#define OMEGA2 315046632 -#define Q2_CRT_CST 292938863 // (Q1*Q3*Q4)^-1 mod Q2 - -#define Q3 ((1u << 30) - 23 * (1u << 17) + 1) -#define OMEGA3 309185662 -#define Q3_CRT_CST 594011630 // (Q1*Q2*Q4)^-1 mod Q3 - -#define Q4 ((1u << 30) - 42 * (1u << 17) + 1) -#define OMEGA4 846468380 -#define Q4_CRT_CST 140177212 // (Q1*Q2*Q3)^-1 mod Q4 -#endif - -/** - * 31-bit primes and 2*2^16 roots of unity - */ -#ifdef SPQLIOS_Q120_USE_31_BIT_PRIMES -#define Q1 ((1u << 31) - 1 * (1u << 17) + 1) -#define OMEGA1 1615402923 -#define Q1_CRT_CST 1811422063 // (Q2*Q3*Q4)^-1 mod Q1 - -#define Q2 ((1u << 31) - 4 * (1u << 17) + 1) -#define OMEGA2 1137738560 -#define Q2_CRT_CST 2093150204 // (Q1*Q3*Q4)^-1 mod Q2 - -#define Q3 ((1u << 31) - 11 * (1u << 17) + 1) -#define OMEGA3 154880552 -#define Q3_CRT_CST 164149010 // (Q1*Q2*Q4)^-1 mod Q3 - -#define Q4 ((1u << 31) - 23 * (1u << 17) + 1) -#define OMEGA4 558784885 -#define Q4_CRT_CST 225197446 // (Q1*Q2*Q3)^-1 mod Q4 -#endif - -static const uint32_t PRIMES_VEC[4] = {Q1, Q2, Q3, Q4}; -static const uint32_t OMEGAS_VEC[4] = {OMEGA1, OMEGA2, OMEGA3, OMEGA4}; - -#define MAX_ELL 10000 - -// each number x mod Q120 is represented by uint64_t[4] with (non-unique) values (x mod q1, x mod q2,x mod q3,x mod q4), -// each between [0 and 2^32-1] -typedef struct _q120a q120a; - -// each number x mod Q120 is represented by uint64_t[4] with (non-unique) values (x mod q1, x mod q2,x mod q3,x mod q4), -// each between [0 and 2^64-1] -typedef struct _q120b q120b; - -// each number x mod Q120 is represented by uint32_t[8] with values (x mod q1, 2^32x mod q1, x mod q2, 2^32.x mod q2, x -// mod q3, 2^32.x mod q3, x mod q4, 2^32.x mod q4) each between [0 and 2^32-1] -typedef struct _q120c q120c; - -typedef struct _q120x2b q120x2b; -typedef struct _q120x2c q120x2c; - -#endif // SPQLIOS_Q120_COMMON_H
D spqlios/lib/spqlios/q120/q120_fallbacks_aarch64.c

@@ -1,5 +0,0 @@

-#include "q120_ntt_private.h" - -EXPORT void q120_ntt_bb_avx2(const q120_ntt_precomp* const precomp, q120b* const data) { UNDEFINED(); } - -EXPORT void q120_intt_bb_avx2(const q120_ntt_precomp* const precomp, q120b* const data) { UNDEFINED(); }
D spqlios/lib/spqlios/q120/q120_ntt.c

@@ -1,340 +0,0 @@

-#include <assert.h> -#include <inttypes.h> -#include <math.h> -#include <stdint.h> - -#include "q120_ntt_private.h" - -q120_ntt_precomp* new_precomp(const uint64_t n) { - q120_ntt_precomp* precomp = malloc(sizeof(*precomp)); - precomp->n = n; - - assert(n && !(n & (n - 1)) && n <= (1 << 16)); // n is a power of 2 smaller than 2^16 - const uint64_t logN = ceil(log2(n)); - precomp->level_metadata = malloc((logN + 2) * sizeof(*precomp->level_metadata)); - - precomp->powomega = spqlios_alloc_custom_align(32, 4 * 2 * n * sizeof(*(precomp->powomega))); - - return precomp; -} - -uint32_t modq_pow(const uint32_t x, const int64_t n, const uint32_t q) { - uint64_t np = (n % (q - 1) + q - 1) % (q - 1); - - uint64_t val_pow = x; - uint64_t res = 1; - while (np != 0) { - if (np & 1) res = (res * val_pow) % q; - val_pow = (val_pow * val_pow) % q; - np >>= 1; - } - return res; -} - -void fill_omegas(const uint64_t n, uint32_t omegas[4]) { - for (uint64_t k = 0; k < 4; ++k) { - omegas[k] = modq_pow(OMEGAS_VEC[k], (1 << 16) / n, PRIMES_VEC[k]); - } - -#ifndef NDEBUG - - const uint64_t logQ = ceil(log2(PRIMES_VEC[0])); - for (int k = 1; k < 4; ++k) { - if (logQ != ceil(log2(PRIMES_VEC[k]))) { - fprintf(stderr, "The 4 primes must have the same bit-size\n"); - exit(-1); - } - } - - // check if each omega is a 2.n primitive root of unity - for (uint64_t k = 0; k < 4; ++k) { - assert(modq_pow(omegas[k], 2 * n, PRIMES_VEC[k]) == 1); - for (uint64_t i = 1; i < 2 * n; ++i) { - assert(modq_pow(omegas[k], i, PRIMES_VEC[k]) != 1); - } - } - - if (logQ > 31) { - fprintf(stderr, "Modulus q bit-size is larger than 30 bit\n"); - exit(-1); - } -#endif -} - -uint64_t fill_reduction_meta(const uint64_t bs_start, q120_ntt_reduc_step_precomp* reduc_metadata) { - // fill reduction metadata - uint64_t bs_after_reduc = -1; - { - uint64_t min_h = -1; - - for (uint64_t h = bs_start / 2; h < bs_start; ++h) { - uint64_t t = 0; - for (uint64_t k = 0; k < 4; ++k) { - const uint64_t t1 = bs_start - h + (uint64_t)ceil(log2((UINT64_C(1) << h) % PRIMES_VEC[k])); - const uint64_t t2 = UINT64_C(1) + ((t1 > h) ? t1 : h); - if (t < t2) t = t2; - } - if (t < bs_after_reduc) { - min_h = h; - bs_after_reduc = t; - } - } - - reduc_metadata->h = min_h; - reduc_metadata->mask = (UINT64_C(1) << min_h) - 1; - for (uint64_t k = 0; k < 4; ++k) { - reduc_metadata->modulo_red_cst[k] = (UINT64_C(1) << min_h) % PRIMES_VEC[k]; - } - - assert(bs_after_reduc < 64); - } - - return bs_after_reduc; -} - -uint64_t round_up_half_n(const uint64_t n) { return (n + 1) / 2; } - -EXPORT q120_ntt_precomp* q120_new_ntt_bb_precomp(const uint64_t n) { - uint32_t omega_vec[4]; - fill_omegas(n, omega_vec); - - const uint64_t logQ = ceil(log2(PRIMES_VEC[0])); - - q120_ntt_precomp* precomp = new_precomp(n); - - uint64_t bs = precomp->input_bit_size = 64; - - LOG("NTT parameters:\n"); - LOG("\tsize = %" PRIu64 "\n", n) - LOG("\tlogQ = %" PRIu64 "\n", logQ); - LOG("\tinput bit-size = %" PRIu64 "\n", bs); - - if (n == 1) return precomp; - - // fill reduction metadata - uint64_t bs_after_reduc = fill_reduction_meta(bs, &(precomp->reduc_metadata)); - - // forward metadata - q120_ntt_step_precomp* level_metadata_ptr = precomp->level_metadata; - - // first level a_k.omega^k - { - const uint64_t half_bs = (bs + 1) / 2; - level_metadata_ptr->half_bs = half_bs; - level_metadata_ptr->mask = (UINT64_C(1) << half_bs) - UINT64_C(1); - level_metadata_ptr->bs = bs = half_bs + logQ + 1; - LOG("\tlevel %6" PRIu64 " output bit-size = %" PRIu64 " (a_k.omega^k) \n", n, bs); - level_metadata_ptr++; - } - - for (uint64_t nn = n; nn >= 4; nn /= 2) { - level_metadata_ptr->reduce = (bs == 64); - if (level_metadata_ptr->reduce) { - bs = bs_after_reduc; - LOG("\treduce output bit-size = %" PRIu64 "\n", bs); - } - - for (int k = 0; k < 4; ++k) level_metadata_ptr->q2bs[k] = (uint64_t)PRIMES_VEC[k] << (bs - logQ); - - double bs_1 = bs + 1; // bit-size of term a+b or a-b - - const uint64_t half_bs = round_up_half_n(bs_1); - uint64_t bs_2 = half_bs + logQ + 1; // bit-size of term (a-b).omega^k - bs = (bs_1 > bs_2) ? bs_1 : bs_2; - assert(bs <= 64); - - level_metadata_ptr->bs = bs; - level_metadata_ptr->half_bs = half_bs; - level_metadata_ptr->mask = (UINT64_C(1) << half_bs) - UINT64_C(1); - level_metadata_ptr++; - - LOG("\tlevel %6" PRIu64 " output bit-size = %" PRIu64 "\n", nn / 2, bs); - } - - // last level (a-b, a+b) - { - level_metadata_ptr->reduce = (bs == 64); - if (level_metadata_ptr->reduce) { - bs = bs_after_reduc; - LOG("\treduce output bit-size = %" PRIu64 "\n", bs); - } - for (int k = 0; k < 4; ++k) level_metadata_ptr->q2bs[k] = ((uint64_t)PRIMES_VEC[k] << (bs - logQ)); - level_metadata_ptr->bs = ++bs; - level_metadata_ptr->half_bs = level_metadata_ptr->mask = UINT64_C(0); // not used - - LOG("\tlevel %6" PRIu64 " output bit-size = %" PRIu64 "\n", UINT64_C(1), bs); - } - precomp->output_bit_size = bs; - - // omega powers - uint64_t* powomega = malloc(sizeof(*powomega) * 2 * n); - for (uint64_t k = 0; k < 4; ++k) { - const uint64_t q = PRIMES_VEC[k]; - - for (uint64_t i = 0; i < 2 * n; ++i) { - powomega[i] = modq_pow(omega_vec[k], i, q); - } - - uint64_t* powomega_ptr = precomp->powomega + k; - level_metadata_ptr = precomp->level_metadata; - - { - // const uint64_t hpow = UINT64_C(1) << level_metadata_ptr->half_bs; - for (uint64_t i = 0; i < n; ++i) { - uint64_t t = powomega[i]; - uint64_t t1 = (t << level_metadata_ptr->half_bs) % q; - powomega_ptr[4 * i] = (t1 << 32) + t; - } - powomega_ptr += 4 * n; - level_metadata_ptr++; - } - - for (uint64_t nn = n; nn >= 4; nn /= 2) { - const uint64_t halfnn = nn / 2; - const uint64_t m = n / halfnn; - - // const uint64_t hpow = UINT64_C(1) << level_metadata_ptr->half_bs; - for (uint64_t i = 1; i < halfnn; ++i) { - uint64_t t = powomega[i * m]; - uint64_t t1 = (t << level_metadata_ptr->half_bs) % q; - powomega_ptr[4 * (i - 1)] = (t1 << 32) + t; - } - powomega_ptr += 4 * (halfnn - 1); - level_metadata_ptr++; - } - } - free(powomega); - - return precomp; -} - -EXPORT q120_ntt_precomp* q120_new_intt_bb_precomp(const uint64_t n) { - uint32_t omega_vec[4]; - fill_omegas(n, omega_vec); - - const uint64_t logQ = ceil(log2(PRIMES_VEC[0])); - - q120_ntt_precomp* precomp = new_precomp(n); - - uint64_t bs = precomp->input_bit_size = 64; - - LOG("iNTT parameters:\n"); - LOG("\tsize = %" PRIu64 "\n", n) - LOG("\tlogQ = %" PRIu64 "\n", logQ); - LOG("\tinput bit-size = %" PRIu64 "\n", bs); - - if (n == 1) return precomp; - - // fill reduction metadata - uint64_t bs_after_reduc = fill_reduction_meta(bs, &(precomp->reduc_metadata)); - - // backward metadata - q120_ntt_step_precomp* level_metadata_ptr = precomp->level_metadata; - - // first level (a+b, a-b) adds 1-bit - { - level_metadata_ptr->reduce = (bs == 64); - if (level_metadata_ptr->reduce) { - bs = bs_after_reduc; - LOG("\treduce output bit-size = %" PRIu64 "\n", bs); - } - - for (int k = 0; k < 4; ++k) level_metadata_ptr->q2bs[k] = (uint64_t)PRIMES_VEC[k] << (bs - logQ); - - level_metadata_ptr->bs = ++bs; - level_metadata_ptr->half_bs = level_metadata_ptr->mask = UINT64_C(0); // not used - LOG("\tlevel %6" PRIu64 " output bit-size = %" PRIu64 "\n", UINT64_C(1), bs); - level_metadata_ptr++; - } - - for (uint64_t nn = 4; nn <= n; nn *= 2) { - level_metadata_ptr->reduce = (bs == 64); - if (level_metadata_ptr->reduce) { - bs = bs_after_reduc; - LOG("\treduce output bit-size = %" PRIu64 "\n", bs); - } - - const uint64_t half_bs = round_up_half_n(bs); - const uint64_t bs_mult = half_bs + logQ + 1; // bit-size of term b.omega^k - bs = 1 + ((bs > bs_mult) ? bs : bs_mult); // bit-size of a+b.omega^k or a-b.omega^k - assert(bs <= 64); - - level_metadata_ptr->bs = bs; - level_metadata_ptr->half_bs = half_bs; - level_metadata_ptr->mask = (UINT64_C(1) << half_bs) - UINT64_C(1); - for (int k = 0; k < 4; ++k) level_metadata_ptr->q2bs[k] = (uint64_t)PRIMES_VEC[k] << (bs_mult - logQ); - level_metadata_ptr++; - - LOG("\tlevel %6" PRIu64 " output bit-size = %" PRIu64 "\n", nn / 2, bs); - } - - // last level a_k.omega^k - { - level_metadata_ptr->reduce = (bs == 64); - if (level_metadata_ptr->reduce) { - bs = bs_after_reduc; - LOG("\treduce output bit-size = %" PRIu64 "\n", bs); - } - - const uint64_t half_bs = round_up_half_n(bs); - - bs = half_bs + logQ + 1; // bit-size of term a.omega^k - assert(bs <= 64); - - level_metadata_ptr->bs = bs; - level_metadata_ptr->half_bs = half_bs; - level_metadata_ptr->mask = (UINT64_C(1) << half_bs) - UINT64_C(1); - for (int k = 0; k < 4; ++k) level_metadata_ptr->q2bs[k] = (uint64_t)PRIMES_VEC[k] << (bs - logQ); - - LOG("\tlevel %6" PRIu64 " output bit-size = %" PRIu64 "\n", n, bs); - } - - // omega powers - uint32_t* powomegabar = malloc(sizeof(*powomegabar) * 2 * n); - for (int k = 0; k < 4; ++k) { - const uint64_t q = PRIMES_VEC[k]; - - for (uint64_t i = 0; i < 2 * n; ++i) { - powomegabar[i] = modq_pow(omega_vec[k], -i, q); - } - - uint64_t* powomega_ptr = precomp->powomega + k; - level_metadata_ptr = precomp->level_metadata + 1; - - for (uint64_t nn = 4; nn <= n; nn *= 2) { - const uint64_t halfnn = nn / 2; - const uint64_t m = n / halfnn; - - for (uint64_t i = 1; i < halfnn; ++i) { - uint64_t t = powomegabar[i * m]; - uint64_t t1 = (t << level_metadata_ptr->half_bs) % q; - powomega_ptr[4 * (i - 1)] = (t1 << 32) + t; - } - powomega_ptr += 4 * (halfnn - 1); - level_metadata_ptr++; - } - - { - const uint64_t invNmod = modq_pow(n, -1, q); - for (uint64_t i = 0; i < n; ++i) { - uint64_t t = (powomegabar[i] * invNmod) % q; - uint64_t t1 = (t << level_metadata_ptr->half_bs) % q; - powomega_ptr[4 * i] = (t1 << 32) + t; - } - } - } - - free(powomegabar); - - return precomp; -} - -void del_precomp(q120_ntt_precomp* precomp) { - spqlios_free(precomp->powomega); - free(precomp->level_metadata); - free(precomp); -} - -EXPORT void q120_del_ntt_bb_precomp(q120_ntt_precomp* precomp) { del_precomp(precomp); } - -EXPORT void q120_del_intt_bb_precomp(q120_ntt_precomp* precomp) { del_precomp(precomp); }
D spqlios/lib/spqlios/q120/q120_ntt.h

@@ -1,25 +0,0 @@

-#ifndef SPQLIOS_Q120_NTT_H -#define SPQLIOS_Q120_NTT_H - -#include "../commons.h" -#include "q120_common.h" - -typedef struct _q120_ntt_precomp q120_ntt_precomp; - -EXPORT q120_ntt_precomp* q120_new_ntt_bb_precomp(const uint64_t n); -EXPORT void q120_del_ntt_bb_precomp(q120_ntt_precomp* precomp); - -EXPORT q120_ntt_precomp* q120_new_intt_bb_precomp(const uint64_t n); -EXPORT void q120_del_intt_bb_precomp(q120_ntt_precomp* precomp); - -/** - * @brief computes a direct ntt in-place over data. - */ -EXPORT void q120_ntt_bb_avx2(const q120_ntt_precomp* const precomp, q120b* const data); - -/** - * @brief computes an inverse ntt in-place over data. - */ -EXPORT void q120_intt_bb_avx2(const q120_ntt_precomp* const precomp, q120b* const data); - -#endif // SPQLIOS_Q120_NTT_H
D spqlios/lib/spqlios/q120/q120_ntt_avx2.c

@@ -1,479 +0,0 @@

-#include <assert.h> -#include <immintrin.h> -#include <inttypes.h> -#include <math.h> -#include <stdint.h> -#include <stdio.h> - -#include "q120_common.h" -#include "q120_ntt_private.h" - -// at which level to switch from computations by level to computations by block -#define CHANGE_MODE_N 1024 - -__always_inline __m256i split_precompmul_si256(__m256i inp, __m256i powomega, const uint64_t h, const __m256i mask) { - const __m256i inp_low = _mm256_and_si256(inp, mask); - const __m256i t1 = _mm256_mul_epu32(inp_low, powomega); - - const __m256i inp_high = _mm256_srli_epi64(inp, h); - const __m256i powomega_high = _mm256_srli_epi64(powomega, 32); - const __m256i t2 = _mm256_mul_epu32(inp_high, powomega_high); - - return _mm256_add_epi64(t1, t2); -} - -__always_inline __m256i modq_red(const __m256i x, const uint64_t h, const __m256i mask, const __m256i _2_pow_h_modq) { - __m256i xh = _mm256_srli_epi64(x, h); - __m256i xl = _mm256_and_si256(x, mask); - __m256i xh_1 = _mm256_mul_epu32(xh, _2_pow_h_modq); - return _mm256_add_epi64(xl, xh_1); -} - -void print_data(const uint64_t n, const uint64_t* const data, const uint64_t q) { - for (uint64_t i = 0; i < n; i++) { - printf("%" PRIu64 " ", *(data + i) % q); - } - printf("\n"); -} - -double max_bit_size(const void* const begin, const void* const end) { - double bs = 0; - const uint64_t* data = (uint64_t*)begin; - while (data != end) { - double t = log2(*(data++)); - if (bs < t) { - bs = t; - } - } - return bs; -} - -void ntt_iter_first(__m256i* const begin, const __m256i* const end, const q120_ntt_step_precomp* const itData, - const __m256i* powomega) { - const uint64_t h = itData->half_bs; - const __m256i vmask = _mm256_set1_epi64x(itData->mask); - - __m256i* data = begin; - while (data < end) { - __m256i x = _mm256_loadu_si256(data); - __m256i po = _mm256_loadu_si256(powomega); - __m256i r = split_precompmul_si256(x, po, h, vmask); - _mm256_storeu_si256(data, r); - - data++; - powomega++; - } -} - -void ntt_iter(const uint64_t nn, __m256i* const begin, const __m256i* const end, - const q120_ntt_step_precomp* const itData, const __m256i* const powomega) { - assert(nn % 2 == 0); - const uint64_t halfnn = nn / 2; - - const __m256i vq2bs = _mm256_loadu_si256((__m256i*)itData->q2bs); - const __m256i vmask = _mm256_set1_epi64x(itData->mask); - - __m256i* data = begin; - while (data < end) { - __m256i* ptr1 = data; - __m256i* ptr2 = data + halfnn; - - const __m256i a = _mm256_loadu_si256(ptr1); - const __m256i b = _mm256_loadu_si256(ptr2); - - const __m256i ap = _mm256_add_epi64(a, b); - _mm256_storeu_si256(ptr1, ap); - - const __m256i bp = _mm256_sub_epi64(_mm256_add_epi64(a, vq2bs), b); - _mm256_storeu_si256(ptr2, bp); - - ptr1++; - ptr2++; - - const __m256i* po_ptr = powomega; - for (uint64_t i = 1; i < halfnn; ++i) { - __m256i a = _mm256_loadu_si256(ptr1); - __m256i b = _mm256_loadu_si256(ptr2); - - __m256i ap = _mm256_add_epi64(a, b); - - _mm256_storeu_si256(ptr1, ap); - - __m256i b1 = _mm256_sub_epi64(_mm256_add_epi64(a, vq2bs), b); - __m256i po = _mm256_loadu_si256(po_ptr); - - __m256i bp = split_precompmul_si256(b1, po, itData->half_bs, vmask); - - _mm256_storeu_si256(ptr2, bp); - - ptr1++; - ptr2++; - po_ptr++; - } - data += nn; - } -} - -void ntt_iter_red(const uint64_t nn, __m256i* const begin, const __m256i* const end, - const q120_ntt_step_precomp* const itData, const __m256i* const powomega, - const q120_ntt_reduc_step_precomp* const reduc_precomp) { - assert(nn % 2 == 0); - const uint64_t halfnn = nn / 2; - - const __m256i vq2bs = _mm256_loadu_si256((__m256i*)itData->q2bs); - const __m256i vmask = _mm256_set1_epi64x(itData->mask); - - const __m256i reduc_mask = _mm256_set1_epi64x(reduc_precomp->mask); - const __m256i reduc_cst = _mm256_loadu_si256((__m256i*)reduc_precomp->modulo_red_cst); - - __m256i* data = begin; - while (data < end) { - __m256i* ptr1 = data; - __m256i* ptr2 = data + halfnn; - - __m256i a = _mm256_loadu_si256(ptr1); - __m256i b = _mm256_loadu_si256(ptr2); - - a = modq_red(a, reduc_precomp->h, reduc_mask, reduc_cst); - b = modq_red(b, reduc_precomp->h, reduc_mask, reduc_cst); - - const __m256i ap = _mm256_add_epi64(a, b); - _mm256_storeu_si256(ptr1, ap); - - const __m256i bp = _mm256_sub_epi64(_mm256_add_epi64(a, vq2bs), b); - _mm256_storeu_si256(ptr2, bp); - - ptr1++; - ptr2++; - - const __m256i* po_ptr = powomega; - for (uint64_t i = 1; i < halfnn; ++i) { - __m256i a = _mm256_loadu_si256(ptr1); - __m256i b = _mm256_loadu_si256(ptr2); - - a = modq_red(a, reduc_precomp->h, reduc_mask, reduc_cst); - b = modq_red(b, reduc_precomp->h, reduc_mask, reduc_cst); - - __m256i ap = _mm256_add_epi64(a, b); - - _mm256_storeu_si256(ptr1, ap); - - __m256i bp = _mm256_sub_epi64(_mm256_add_epi64(a, vq2bs), b); - __m256i po = _mm256_loadu_si256(po_ptr); - bp = split_precompmul_si256(bp, po, itData->half_bs, vmask); - - _mm256_storeu_si256(ptr2, bp); - - ptr1++; - ptr2++; - po_ptr++; - } - data += nn; - } -} - -EXPORT void q120_ntt_bb_avx2(const q120_ntt_precomp* const precomp, q120b* const data_ptr) { - // assert((size_t)data_ptr % 32 == 0); // alignment check - - const uint64_t n = precomp->n; - if (n == 1) return; - - const q120_ntt_step_precomp* itData = precomp->level_metadata; - const __m256i* powomega = (__m256i*)precomp->powomega; - - __m256i* const begin = (__m256i*)data_ptr; - const __m256i* const end = ((__m256i*)data_ptr) + n; - - if (CHECK_BOUNDS) { - double bs __attribute__((unused)) = max_bit_size((void*)begin, (void*)end); - LOG("Input %lf %" PRIu64 "\n", bs, precomp->input_bit_size); - assert(bs <= precomp->input_bit_size); - } - - // first iteration a_k.omega^k - ntt_iter_first(begin, end, itData, powomega); - - if (CHECK_BOUNDS) { - double bs __attribute__((unused)) = max_bit_size((void*)begin, (void*)end); - LOG("Iter %3" PRIu64 " - %lf %" PRIu64 "\n", n, bs, itData->bs); - assert(bs < itData->bs); - } - - powomega += n; - itData++; - - const uint64_t split_nn = (CHANGE_MODE_N > n) ? n : CHANGE_MODE_N; - // const uint64_t split_nn = 2; - - // computations by level - for (uint64_t nn = n; nn > split_nn; nn /= 2) { - const uint64_t halfnn = nn / 2; - - if (itData->reduce) { - ntt_iter_red(nn, begin, end, itData, powomega, &precomp->reduc_metadata); - } else { - ntt_iter(nn, begin, end, itData, powomega); - } - - if (CHECK_BOUNDS) { - double bs __attribute__((unused)) = max_bit_size((void*)begin, (void*)end); - LOG("Iter %3" PRIu64 " - %lf %" PRIu64 " %c\n", nn / 2, bs, itData->bs, itData->reduce ? '*' : ' '); - assert(bs < itData->bs); - } - - powomega += halfnn - 1; - itData++; - } - - // computations by memory block - if (split_nn >= 2) { - const q120_ntt_step_precomp* itData1 = itData; - const __m256i* powomega1 = powomega; - for (__m256i* it = begin; it < end; it += split_nn) { - __m256i* const begin1 = it; - const __m256i* const end1 = it + split_nn; - - itData = itData1; - powomega = powomega1; - for (uint64_t nn = split_nn; nn >= 2; nn /= 2) { - const uint64_t halfnn = nn / 2; - - if (itData->reduce) { - ntt_iter_red(nn, begin1, end1, itData, powomega, &precomp->reduc_metadata); - } else { - ntt_iter(nn, begin1, end1, itData, powomega); - } - - if (CHECK_BOUNDS) { - double bs __attribute__((unused)) = max_bit_size((uint64_t*)begin1, (uint64_t*)end1); - // LOG("Iter %3lu - %lf %lu\n", nn / 2, bs, itData->bs); - assert(bs < itData->bs); - } - - powomega += halfnn - 1; - itData++; - } - } - } - - if (CHECK_BOUNDS) { - double bs __attribute__((unused)) = max_bit_size((void*)begin, (void*)end); - LOG("Iter %3" PRIu64 " - %lf %" PRIu64 "\n", UINT64_C(1), bs, precomp->output_bit_size); - assert(bs < precomp->output_bit_size); - } -} - -void intt_iter(const uint64_t nn, __m256i* const begin, const __m256i* const end, - const q120_ntt_step_precomp* const itData, const __m256i* const powomega) { - assert(nn % 2 == 0); - const uint64_t halfnn = nn / 2; - - const __m256i vq2bs = _mm256_loadu_si256((__m256i*)itData->q2bs); - const __m256i vmask = _mm256_set1_epi64x(itData->mask); - - __m256i* data = begin; - while (data < end) { - __m256i* ptr1 = data; - __m256i* ptr2 = data + halfnn; - - const __m256i a = _mm256_loadu_si256(ptr1); - const __m256i b = _mm256_loadu_si256(ptr2); - - const __m256i ap = _mm256_add_epi64(a, b); - _mm256_storeu_si256(ptr1, ap); - - const __m256i bp = _mm256_sub_epi64(_mm256_add_epi64(a, vq2bs), b); - _mm256_storeu_si256(ptr2, bp); - - ptr1++; - ptr2++; - - const __m256i* po_ptr = powomega; - for (uint64_t i = 1; i < halfnn; ++i) { - __m256i a = _mm256_loadu_si256(ptr1); - __m256i b = _mm256_loadu_si256(ptr2); - - __m256i po = _mm256_loadu_si256(po_ptr); - __m256i bo = split_precompmul_si256(b, po, itData->half_bs, vmask); - - __m256i ap = _mm256_add_epi64(a, bo); - - _mm256_storeu_si256(ptr1, ap); - - __m256i bp = _mm256_sub_epi64(_mm256_add_epi64(a, vq2bs), bo); - - _mm256_storeu_si256(ptr2, bp); - - ptr1++; - ptr2++; - po_ptr++; - } - data += nn; - } -} - -void intt_iter_red(const uint64_t nn, __m256i* const begin, const __m256i* const end, - const q120_ntt_step_precomp* const itData, const __m256i* const powomega, - const q120_ntt_reduc_step_precomp* const reduc_precomp) { - assert(nn % 2 == 0); - const uint64_t halfnn = nn / 2; - - const __m256i vq2bs = _mm256_loadu_si256((__m256i*)itData->q2bs); - const __m256i vmask = _mm256_set1_epi64x(itData->mask); - - const __m256i reduc_mask = _mm256_set1_epi64x(reduc_precomp->mask); - const __m256i reduc_cst = _mm256_loadu_si256((__m256i*)reduc_precomp->modulo_red_cst); - - __m256i* data = begin; - while (data < end) { - __m256i* ptr1 = data; - __m256i* ptr2 = data + halfnn; - - __m256i a = _mm256_loadu_si256(ptr1); - __m256i b = _mm256_loadu_si256(ptr2); - - a = modq_red(a, reduc_precomp->h, reduc_mask, reduc_cst); - b = modq_red(b, reduc_precomp->h, reduc_mask, reduc_cst); - - const __m256i ap = _mm256_add_epi64(a, b); - _mm256_storeu_si256(ptr1, ap); - - const __m256i bp = _mm256_sub_epi64(_mm256_add_epi64(a, vq2bs), b); - _mm256_storeu_si256(ptr2, bp); - - ptr1++; - ptr2++; - - const __m256i* po_ptr = powomega; - for (uint64_t i = 1; i < halfnn; ++i) { - __m256i a = _mm256_loadu_si256(ptr1); - __m256i b = _mm256_loadu_si256(ptr2); - - a = modq_red(a, reduc_precomp->h, reduc_mask, reduc_cst); - b = modq_red(b, reduc_precomp->h, reduc_mask, reduc_cst); - - __m256i po = _mm256_loadu_si256(po_ptr); - __m256i bo = split_precompmul_si256(b, po, itData->half_bs, vmask); - - __m256i ap = _mm256_add_epi64(a, bo); - - _mm256_storeu_si256(ptr1, ap); - - __m256i bp = _mm256_sub_epi64(_mm256_add_epi64(a, vq2bs), bo); - - _mm256_storeu_si256(ptr2, bp); - - ptr1++; - ptr2++; - po_ptr++; - } - data += nn; - } -} - -void ntt_iter_first_red(__m256i* const begin, const __m256i* const end, const q120_ntt_step_precomp* const itData, - const __m256i* powomega, const q120_ntt_reduc_step_precomp* const reduc_precomp) { - const uint64_t h = itData->half_bs; - const __m256i vmask = _mm256_set1_epi64x(itData->mask); - - const __m256i reduc_mask = _mm256_set1_epi64x(reduc_precomp->mask); - const __m256i reduc_cst = _mm256_loadu_si256((__m256i*)reduc_precomp->modulo_red_cst); - - __m256i* data = begin; - while (data < end) { - __m256i x = _mm256_loadu_si256(data); - x = modq_red(x, reduc_precomp->h, reduc_mask, reduc_cst); - __m256i po = _mm256_loadu_si256(powomega); - __m256i r = split_precompmul_si256(x, po, h, vmask); - _mm256_storeu_si256(data, r); - - data++; - powomega++; - } -} - -EXPORT void q120_intt_bb_avx2(const q120_ntt_precomp* const precomp, q120b* const data_ptr) { - // assert((size_t)data_ptr % 32 == 0); // alignment check - - const uint64_t n = precomp->n; - if (n == 1) return; - - const q120_ntt_step_precomp* itData = precomp->level_metadata; - const __m256i* powomega = (__m256i*)precomp->powomega; - - __m256i* const begin = (__m256i*)data_ptr; - const __m256i* const end = ((__m256i*)data_ptr) + n; - - if (CHECK_BOUNDS) { - double bs __attribute__((unused)) = max_bit_size((void*)begin, (void*)end); - LOG("Input %lf %" PRIu64 "\n", bs, precomp->input_bit_size); - assert(bs <= precomp->input_bit_size); - } - - const uint64_t split_nn = (CHANGE_MODE_N > n) ? n : CHANGE_MODE_N; - - // computations by memory block - if (split_nn >= 2) { - const q120_ntt_step_precomp* itData1 = itData; - const __m256i* powomega1 = powomega; - for (__m256i* it = begin; it < end; it += split_nn) { - __m256i* const begin1 = it; - const __m256i* const end1 = it + split_nn; - - itData = itData1; - powomega = powomega1; - for (uint64_t nn = 2; nn <= split_nn; nn *= 2) { - const uint64_t halfnn = nn / 2; - - if (itData->reduce) { - intt_iter_red(nn, begin1, end1, itData, powomega, &precomp->reduc_metadata); - } else { - intt_iter(nn, begin1, end1, itData, powomega); - } - - if (CHECK_BOUNDS) { - double bs __attribute__((unused)) = max_bit_size((uint64_t*)begin1, (uint64_t*)end1); - // LOG("Iter %3lu - %lf %lu\n", nn / 2, bs, itData->bs); - assert(bs < itData->bs); - } - - powomega += halfnn - 1; - itData++; - } - } - } - - // computations by level - // for (uint64_t nn = 2; nn <= n; nn *= 2) { - for (uint64_t nn = 2 * split_nn; nn <= n; nn *= 2) { - const uint64_t halfnn = nn / 2; - - if (itData->reduce) { - intt_iter_red(nn, begin, end, itData, powomega, &precomp->reduc_metadata); - } else { - intt_iter(nn, begin, end, itData, powomega); - } - - if (CHECK_BOUNDS) { - double bs __attribute__((unused)) = max_bit_size((void*)begin, (void*)end); - LOG("Iter %3" PRIu64 " - %lf %" PRIu64 " %c\n", nn / 2, bs, itData->bs, itData->reduce ? '*' : ' '); - assert(bs < itData->bs); - } - - powomega += halfnn - 1; - itData++; - } - - // last iteration a_k . omega^k . n^-1 - if (itData->reduce) { - ntt_iter_first_red(begin, end, itData, powomega, &precomp->reduc_metadata); - } else { - ntt_iter_first(begin, end, itData, powomega); - } - - if (CHECK_BOUNDS) { - double bs __attribute__((unused)) = max_bit_size((void*)begin, (void*)end); - LOG("Iter %3" PRIu64 " - %lf %" PRIu64 "\n", n, bs, itData->bs); - assert(bs < itData->bs); - } -}
D spqlios/lib/spqlios/q120/q120_ntt_private.h

@@ -1,39 +0,0 @@

-#include "q120_ntt.h" - -#ifndef NDEBUG -#define CHECK_BOUNDS 1 -#define VERBOSE -#else -#define CHECK_BOUNDS 0 -#endif - -#ifndef VERBOSE -#define LOG(...) ; -#else -#define LOG(...) printf(__VA_ARGS__); -#endif - -typedef struct _q120_ntt_step_precomp { - uint64_t q2bs[4]; // q2bs = 2^{bs-31}.q[k] - uint64_t bs; // inputs at this iterations must be in Q_n - uint64_t half_bs; // == ceil(bs/2) - uint64_t mask; // (1<<half_bs) - 1 - uint8_t reduce; -} q120_ntt_step_precomp; - -typedef struct _q120_ntt_reduc_step_precomp { - uint64_t modulo_red_cst[4]; - uint64_t mask; - uint64_t h; -} q120_ntt_reduc_step_precomp; - -typedef struct _q120_ntt_precomp { - uint64_t n; // NTT size (a power of 2) - - q120_ntt_step_precomp* level_metadata; - uint64_t* powomega; - q120_ntt_reduc_step_precomp reduc_metadata; - - uint64_t input_bit_size; - uint64_t output_bit_size; -} q120_ntt_precomp;
D spqlios/lib/spqlios/reim/reim_conversions.c

@@ -1,212 +0,0 @@

-#include <errno.h> -#include <string.h> - -#include "../commons_private.h" -#include "reim_fft_internal.h" -#include "reim_fft_private.h" - -void reim_from_znx64_ref(const REIM_FROM_ZNX64_PRECOMP* precomp, void* r, const int64_t* x) { - // naive version of the function (just cast) - const uint64_t nn = precomp->m << 1; - double* res = (double*)r; - for (uint64_t i = 0; i < nn; ++i) { - res[i] = (double)x[i]; - } -} - -void* init_reim_from_znx64_precomp(REIM_FROM_ZNX64_PRECOMP* const res, uint32_t m, uint32_t log2bound) { - if (m & (m - 1)) return spqlios_error("m must be a power of 2"); - // currently, we are going to use the trick add 3.2^51, mask the exponent, reinterpret bits as double. - // therefore we need the input values to be < 2^50. - if (log2bound > 50) return spqlios_error("Invalid log2bound error: must be in [0,50]"); - res->m = m; - FROM_ZNX64_FUNC resf = reim_from_znx64_ref; - if (m >= 8) { - if (CPU_SUPPORTS("avx2")) { - resf = reim_from_znx64_bnd50_fma; - } - } - res->function = resf; - return res; -} - -EXPORT REIM_FROM_ZNX64_PRECOMP* new_reim_from_znx64_precomp(uint32_t m, uint32_t log2bound) { - REIM_FROM_ZNX64_PRECOMP* res = malloc(sizeof(*res)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_reim_from_znx64_precomp(res, m, log2bound)); -} - -EXPORT void reim_from_znx64(const REIM_FROM_ZNX64_PRECOMP* tables, void* r, const int64_t* a) { - tables->function(tables, r, a); -} - -/** - * @brief Simpler API for the znx64 to reim conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment - */ -EXPORT void reim_from_znx64_simple(uint32_t m, uint32_t log2bound, void* r, const int64_t* a) { - // not checking for log2bound which is not relevant here - static REIM_FROM_ZNX64_PRECOMP precomp[32]; - REIM_FROM_ZNX64_PRECOMP* p = precomp + log2m(m); - if (!p->function) { - if (!init_reim_from_znx64_precomp(p, m, log2bound)) abort(); - } - p->function(p, r, a); -} - -void reim_from_znx32_ref(const REIM_FROM_ZNX32_PRECOMP* precomp, void* r, const int32_t* x) { NOT_IMPLEMENTED(); } -void reim_from_znx32_avx2_fma(const REIM_FROM_ZNX32_PRECOMP* precomp, void* r, const int32_t* x) { NOT_IMPLEMENTED(); } - -void* init_reim_from_znx32_precomp(REIM_FROM_ZNX32_PRECOMP* const res, uint32_t m, uint32_t log2bound) { - if (m & (m - 1)) return spqlios_error("m must be a power of 2"); - if (log2bound > 32) return spqlios_error("Invalid log2bound error: must be in [0,32]"); - res->m = m; - // TODO: check selection logic - if (CPU_SUPPORTS("avx2")) { - if (m >= 8) { - res->function = reim_from_znx32_avx2_fma; - } else { - res->function = reim_from_znx32_ref; - } - } else { - res->function = reim_from_znx32_ref; - } - return res; -} - -EXPORT REIM_FROM_ZNX32_PRECOMP* new_reim_from_znx32_precomp(uint32_t m, uint32_t log2bound) { - REIM_FROM_ZNX32_PRECOMP* res = malloc(sizeof(*res)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_reim_from_znx32_precomp(res, m, log2bound)); -} - -void reim_from_tnx32_ref(const REIM_FROM_TNX32_PRECOMP* precomp, void* r, const int32_t* x) { NOT_IMPLEMENTED(); } -void reim_from_tnx32_avx2_fma(const REIM_FROM_TNX32_PRECOMP* precomp, void* r, const int32_t* x) { NOT_IMPLEMENTED(); } - -void* init_reim_from_tnx32_precomp(REIM_FROM_TNX32_PRECOMP* const res, uint32_t m) { - if (m & (m - 1)) return spqlios_error("m must be a power of 2"); - res->m = m; - // TODO: check selection logic - if (CPU_SUPPORTS("avx2")) { - if (m >= 8) { - res->function = reim_from_tnx32_avx2_fma; - } else { - res->function = reim_from_tnx32_ref; - } - } else { - res->function = reim_from_tnx32_ref; - } - return res; -} - -EXPORT REIM_FROM_TNX32_PRECOMP* new_reim_from_tnx32_precomp(uint32_t m) { - REIM_FROM_TNX32_PRECOMP* res = malloc(sizeof(*res)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_reim_from_tnx32_precomp(res, m)); -} - -void reim_to_tnx32_ref(const REIM_TO_TNX32_PRECOMP* precomp, int32_t* r, const void* x) { NOT_IMPLEMENTED(); } -void reim_to_tnx32_avx2_fma(const REIM_TO_TNX32_PRECOMP* precomp, int32_t* r, const void* x) { NOT_IMPLEMENTED(); } - -void* init_reim_to_tnx32_precomp(REIM_TO_TNX32_PRECOMP* const res, uint32_t m, double divisor, uint32_t log2overhead) { - if (m & (m - 1)) return spqlios_error("m must be a power of 2"); - if (is_not_pow2_double(&divisor)) return spqlios_error("divisor must be a power of 2"); - if (log2overhead > 52) return spqlios_error("log2overhead is too large"); - res->m = m; - res->divisor = divisor; - // TODO: check selection logic - if (CPU_SUPPORTS("avx2")) { - if (log2overhead <= 18) { - if (m >= 8) { - res->function = reim_to_tnx32_avx2_fma; - } else { - res->function = reim_to_tnx32_ref; - } - } else { - res->function = reim_to_tnx32_ref; - } - } else { - res->function = reim_to_tnx32_ref; - } - return res; -} - -EXPORT REIM_TO_TNX32_PRECOMP* new_reim_to_tnx32_precomp(uint32_t m, double divisor, uint32_t log2overhead) { - REIM_TO_TNX32_PRECOMP* res = malloc(sizeof(*res)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_reim_to_tnx32_precomp(res, m, divisor, log2overhead)); -} - -EXPORT void reim_from_znx32_simple(uint32_t m, uint32_t log2bound, void* r, const int32_t* x) { - static REIM_FROM_ZNX32_PRECOMP* p[31] = {0}; - REIM_FROM_ZNX32_PRECOMP** f = p + log2m(m); - if (!*f) *f = new_reim_from_znx32_precomp(m, log2bound); - (*f)->function(*f, r, x); -} - -EXPORT void reim_from_tnx32_simple(uint32_t m, void* r, const int32_t* x) { - static REIM_FROM_TNX32_PRECOMP* p[31] = {0}; - REIM_FROM_TNX32_PRECOMP** f = p + log2m(m); - if (!*f) *f = new_reim_from_tnx32_precomp(m); - (*f)->function(*f, r, x); -} - -EXPORT void reim_to_tnx32_simple(uint32_t m, double divisor, uint32_t log2overhead, int32_t* r, const void* x) { - static REIM_TO_TNX32_PRECOMP* p[31] = {0}; - REIM_TO_TNX32_PRECOMP** f = p + log2m(m); - if (!*f) *f = new_reim_to_tnx32_precomp(m, divisor, log2overhead); - (*f)->function(*f, r, x); -} - -void reim_to_znx64_ref(const REIM_TO_ZNX64_PRECOMP* precomp, int64_t* r, const void* x) { - // for now, we stick to a slow implem - uint64_t nn = precomp->m << 1; - const double* v = (double*)x; - double invdiv = 1. / precomp->divisor; - for (uint64_t i = 0; i < nn; ++i) { - r[i] = (int64_t)rint(v[i] * invdiv); - } -} - -void* init_reim_to_znx64_precomp(REIM_TO_ZNX64_PRECOMP* const res, uint32_t m, double divisor, uint32_t log2bound) { - if (m & (m - 1)) return spqlios_error("m must be a power of 2"); - if (is_not_pow2_double(&divisor)) return spqlios_error("divisor must be a power of 2"); - if (log2bound > 64) return spqlios_error("log2bound is too large"); - res->m = m; - res->divisor = divisor; - TO_ZNX64_FUNC resf = reim_to_znx64_ref; - if (CPU_SUPPORTS("avx2") && m >= 8) { - if (log2bound <= 50) { - resf = reim_to_znx64_avx2_bnd50_fma; - } else { - resf = reim_to_znx64_avx2_bnd63_fma; - } - } - res->function = resf; // must be the last one set - return res; -} - -EXPORT REIM_TO_ZNX64_PRECOMP* new_reim_to_znx64_precomp(uint32_t m, double divisor, uint32_t log2bound) { - REIM_TO_ZNX64_PRECOMP* res = malloc(sizeof(*res)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_reim_to_znx64_precomp(res, m, divisor, log2bound)); -} - -EXPORT void reim_to_znx64(const REIM_TO_ZNX64_PRECOMP* precomp, int64_t* r, const void* a) { - precomp->function(precomp, r, a); -} - -/** - * @brief Simpler API for the znx64 to reim conversion. - */ -EXPORT void reim_to_znx64_simple(uint32_t m, double divisor, uint32_t log2bound, int64_t* r, const void* a) { - // not checking distinguishing <=50 or not - static __thread REIM_TO_ZNX64_PRECOMP p; - static __thread uint32_t prev_log2bound; - if (!p.function || p.m != m || p.divisor != divisor || prev_log2bound != log2bound) { - if (!init_reim_to_znx64_precomp(&p, m, divisor, log2bound)) abort(); - prev_log2bound = log2bound; - } - p.function(&p, r, a); -}
D spqlios/lib/spqlios/reim/reim_conversions_avx.c

@@ -1,106 +0,0 @@

-#include <immintrin.h> - -#include "reim_fft_private.h" - -void reim_from_znx64_bnd50_fma(const REIM_FROM_ZNX64_PRECOMP* precomp, void* r, const int64_t* x) { - static const double EXPO = INT64_C(1) << 52; - const int64_t ADD_CST = INT64_C(1) << 51; - const double SUB_CST = INT64_C(3) << 51; - - const __m256d SUB_CST_4 = _mm256_set1_pd(SUB_CST); - const __m256i ADD_CST_4 = _mm256_set1_epi64x(ADD_CST); - const __m256d EXPO_4 = _mm256_set1_pd(EXPO); - - double(*out)[4] = (double(*)[4])r; - __m256i* in = (__m256i*)x; - __m256i* inend = (__m256i*)(x + (precomp->m << 1)); - do { - // read the next value - __m256i a = _mm256_loadu_si256(in); - a = _mm256_add_epi64(a, ADD_CST_4); - __m256d ad = _mm256_castsi256_pd(a); - ad = _mm256_or_pd(ad, EXPO_4); - ad = _mm256_sub_pd(ad, SUB_CST_4); - // store the next value - _mm256_storeu_pd(out[0], ad); - ++out; - ++in; - } while (in < inend); -} - -// version where the output norm can be as big as 2^63 -void reim_to_znx64_avx2_bnd63_fma(const REIM_TO_ZNX64_PRECOMP* precomp, int64_t* r, const void* x) { - static const uint64_t SIGN_MASK = 0x8000000000000000UL; - static const uint64_t EXPO_MASK = 0x7FF0000000000000UL; - static const uint64_t MANTISSA_MASK = 0x000FFFFFFFFFFFFFUL; - static const uint64_t MANTISSA_MSB = 0x0010000000000000UL; - const double divisor_bits = precomp->divisor * ((double)(INT64_C(1) << 52)); - const double offset = precomp->divisor / 2.; - - const __m256d SIGN_MASK_4 = _mm256_castsi256_pd(_mm256_set1_epi64x(SIGN_MASK)); - const __m256i EXPO_MASK_4 = _mm256_set1_epi64x(EXPO_MASK); - const __m256i MANTISSA_MASK_4 = _mm256_set1_epi64x(MANTISSA_MASK); - const __m256i MANTISSA_MSB_4 = _mm256_set1_epi64x(MANTISSA_MSB); - const __m256d offset_4 = _mm256_set1_pd(offset); - const __m256i divi_bits_4 = _mm256_castpd_si256(_mm256_set1_pd(divisor_bits)); - - double(*in)[4] = (double(*)[4])x; - __m256i* out = (__m256i*)r; - __m256i* outend = (__m256i*)(r + (precomp->m << 1)); - do { - // read the next value - __m256d a = _mm256_loadu_pd(in[0]); - // a += sign(a) * m/2 - __m256d asign = _mm256_and_pd(a, SIGN_MASK_4); - a = _mm256_add_pd(a, _mm256_or_pd(asign, offset_4)); - // sign: either 0 or -1 - __m256i sign_mask = _mm256_castpd_si256(asign); - sign_mask = _mm256_sub_epi64(_mm256_set1_epi64x(0), _mm256_srli_epi64(sign_mask, 63)); - // compute the exponents - __m256i a0exp = _mm256_and_si256(_mm256_castpd_si256(a), EXPO_MASK_4); - __m256i a0lsh = _mm256_sub_epi64(a0exp, divi_bits_4); - __m256i a0rsh = _mm256_sub_epi64(divi_bits_4, a0exp); - a0lsh = _mm256_srli_epi64(a0lsh, 52); - a0rsh = _mm256_srli_epi64(a0rsh, 52); - // compute the new mantissa - __m256i a0pos = _mm256_and_si256(_mm256_castpd_si256(a), MANTISSA_MASK_4); - a0pos = _mm256_or_si256(a0pos, MANTISSA_MSB_4); - a0lsh = _mm256_sllv_epi64(a0pos, a0lsh); - a0rsh = _mm256_srlv_epi64(a0pos, a0rsh); - __m256i final = _mm256_or_si256(a0lsh, a0rsh); - // negate if the sign was negative - final = _mm256_xor_si256(final, sign_mask); - final = _mm256_sub_epi64(final, sign_mask); - // read the next value - _mm256_storeu_si256(out, final); - ++out; - ++in; - } while (out < outend); -} - -// version where the output norm can be as big as 2^50 -void reim_to_znx64_avx2_bnd50_fma(const REIM_TO_ZNX64_PRECOMP* precomp, int64_t* r, const void* x) { - static const uint64_t MANTISSA_MASK = 0x000FFFFFFFFFFFFFUL; - const int64_t SUB_CST = INT64_C(1) << 51; - const double add_cst = precomp->divisor * ((double)(INT64_C(3) << 51)); - - const __m256i SUB_CST_4 = _mm256_set1_epi64x(SUB_CST); - const __m256d add_cst_4 = _mm256_set1_pd(add_cst); - const __m256i MANTISSA_MASK_4 = _mm256_set1_epi64x(MANTISSA_MASK); - - double(*in)[4] = (double(*)[4])x; - __m256i* out = (__m256i*)r; - __m256i* outend = (__m256i*)(r + (precomp->m << 1)); - do { - // read the next value - __m256d a = _mm256_loadu_pd(in[0]); - a = _mm256_add_pd(a, add_cst_4); - __m256i ai = _mm256_castpd_si256(a); - ai = _mm256_and_si256(ai, MANTISSA_MASK_4); - ai = _mm256_sub_epi64(ai, SUB_CST_4); - // store the next value - _mm256_storeu_si256(out, ai); - ++out; - ++in; - } while (out < outend); -}
D spqlios/lib/spqlios/reim/reim_execute.c

@@ -1,22 +0,0 @@

-#include "reim_fft_internal.h" -#include "reim_fft_private.h" - -EXPORT void reim_from_znx32(const REIM_FROM_ZNX32_PRECOMP* tables, void* r, const int32_t* a) { - tables->function(tables, r, a); -} - -EXPORT void reim_from_tnx32(const REIM_FROM_TNX32_PRECOMP* tables, void* r, const int32_t* a) { - tables->function(tables, r, a); -} - -EXPORT void reim_to_tnx32(const REIM_TO_TNX32_PRECOMP* tables, int32_t* r, const void* a) { - tables->function(tables, r, a); -} - -EXPORT void reim_fftvec_mul(const REIM_FFTVEC_MUL_PRECOMP* tables, double* r, const double* a, const double* b) { - tables->function(tables, r, a, b); -} - -EXPORT void reim_fftvec_addmul(const REIM_FFTVEC_ADDMUL_PRECOMP* tables, double* r, const double* a, const double* b) { - tables->function(tables, r, a, b); -}
D spqlios/lib/spqlios/reim/reim_fallbacks_aarch64.c

@@ -1,15 +0,0 @@

-#include "reim_fft_private.h" - -EXPORT void reim_fftvec_addmul_fma(const REIM_FFTVEC_ADDMUL_PRECOMP* precomp, double* r, const double* a, - const double* b) { - UNDEFINED(); -} -EXPORT void reim_fftvec_mul_fma(const REIM_FFTVEC_MUL_PRECOMP* precomp, double* r, const double* a, const double* b) { - UNDEFINED(); -} - -EXPORT void reim_fft_avx2_fma(const REIM_FFT_PRECOMP* tables, double* data) { UNDEFINED(); } -EXPORT void reim_ifft_avx2_fma(const REIM_IFFT_PRECOMP* tables, double* data) { UNDEFINED(); } - -//EXPORT void reim_fft(const REIM_FFT_PRECOMP* tables, double* data) { tables->function(tables, data); } -//EXPORT void reim_ifft(const REIM_IFFT_PRECOMP* tables, double* data) { tables->function(tables, data); }
D spqlios/lib/spqlios/reim/reim_fft.h

@@ -1,207 +0,0 @@

-#ifndef SPQLIOS_REIM_FFT_H -#define SPQLIOS_REIM_FFT_H - -#include "../commons.h" - -typedef struct reim_fft_precomp REIM_FFT_PRECOMP; -typedef struct reim_ifft_precomp REIM_IFFT_PRECOMP; -typedef struct reim_mul_precomp REIM_FFTVEC_MUL_PRECOMP; -typedef struct reim_addmul_precomp REIM_FFTVEC_ADDMUL_PRECOMP; -typedef struct reim_from_znx32_precomp REIM_FROM_ZNX32_PRECOMP; -typedef struct reim_from_znx64_precomp REIM_FROM_ZNX64_PRECOMP; -typedef struct reim_from_tnx32_precomp REIM_FROM_TNX32_PRECOMP; -typedef struct reim_to_tnx32_precomp REIM_TO_TNX32_PRECOMP; -typedef struct reim_to_tnx_precomp REIM_TO_TNX_PRECOMP; -typedef struct reim_to_znx64_precomp REIM_TO_ZNX64_PRECOMP; - -/** - * @brief precomputes fft tables. - * The FFT tables contains a constant section that is required for efficient FFT operations in dimension nn. - * The resulting pointer is to be passed as "tables" argument to any call to the fft function. - * The user can optionnally allocate zero or more computation buffers, which are scratch spaces that are contiguous to - * the constant tables in memory, and allow for more efficient operations. It is the user's responsibility to ensure - * that each of those buffers are never used simultaneously by two ffts on different threads at the same time. The fft - * table must be deleted by delete_fft_precomp after its last usage. - */ -EXPORT REIM_FFT_PRECOMP* new_reim_fft_precomp(uint32_t m, uint32_t num_buffers); - -/** - * @brief gets the address of a fft buffer allocated during new_fft_precomp. - * This buffer can be used as data pointer in subsequent calls to fft, - * and does not need to be released afterwards. - */ -EXPORT double* reim_fft_precomp_get_buffer(const REIM_FFT_PRECOMP* tables, uint32_t buffer_index); - -/** - * @brief allocates a new fft buffer. - * This buffer can be used as data pointer in subsequent calls to fft, - * and must be deleted afterwards by calling delete_fft_buffer. - */ -EXPORT double* new_reim_fft_buffer(uint32_t m); - -/** - * @brief allocates a new fft buffer. - * This buffer can be used as data pointer in subsequent calls to fft, - * and must be deleted afterwards by calling delete_fft_buffer. - */ -EXPORT void delete_reim_fft_buffer(double* buffer); - -/** - * @brief deallocates a fft table and all its built-in buffers. - */ -#define delete_reim_fft_precomp free - -/** - * @brief computes a direct fft in-place over data. - */ -EXPORT void reim_fft(const REIM_FFT_PRECOMP* tables, double* data); - -EXPORT REIM_IFFT_PRECOMP* new_reim_ifft_precomp(uint32_t m, uint32_t num_buffers); -EXPORT double* reim_ifft_precomp_get_buffer(const REIM_IFFT_PRECOMP* tables, uint32_t buffer_index); -EXPORT void reim_ifft(const REIM_IFFT_PRECOMP* tables, double* data); -#define delete_reim_ifft_precomp free - -EXPORT REIM_FFTVEC_MUL_PRECOMP* new_reim_fftvec_mul_precomp(uint32_t m); -EXPORT void reim_fftvec_mul(const REIM_FFTVEC_MUL_PRECOMP* tables, double* r, const double* a, const double* b); -#define delete_reim_fftvec_mul_precomp free - -EXPORT REIM_FFTVEC_ADDMUL_PRECOMP* new_reim_fftvec_addmul_precomp(uint32_t m); -EXPORT void reim_fftvec_addmul(const REIM_FFTVEC_ADDMUL_PRECOMP* tables, double* r, const double* a, const double* b); -#define delete_reim_fftvec_addmul_precomp free - -/** - * @brief prepares a conversion from ZnX to the cplx layout. - * All the coefficients must be strictly lower than 2^log2bound in absolute value. Any attempt to use - * this function on a larger coefficient is undefined behaviour. The resulting precomputed data must - * be freed with `new_reim_from_znx32_precomp` - * @param m the target complex dimension m from C[X] mod X^m-i. Note that the inputs have n=2m - * int32 coefficients in natural order modulo X^n+1 - * @param log2bound bound on the input coefficients. Must be between 0 and 32 - */ -EXPORT REIM_FROM_ZNX32_PRECOMP* new_reim_from_znx32_precomp(uint32_t m, uint32_t log2bound); - -/** - * @brief converts from ZnX to the cplx layout. - * @param tables precomputed data obtained by new_reim_from_znx32_precomp. - * @param r resulting array of m complexes coefficients mod X^m-i - * @param x input array of n bounded integer coefficients mod X^n+1 - */ -EXPORT void reim_from_znx32(const REIM_FROM_ZNX32_PRECOMP* tables, void* r, const int32_t* a); -/** @brief frees a precomputed conversion data initialized with new_reim_from_znx32_precomp. */ -#define delete_reim_from_znx32_precomp free - -/** - * @brief converts from ZnX to the cplx layout. - * @param tables precomputed data obtained by new_reim_from_znx64_precomp. - * @param r resulting array of m complexes coefficients mod X^m-i - * @param x input array of n bounded integer coefficients mod X^n+1 - */ -EXPORT void reim_from_znx64(const REIM_FROM_ZNX64_PRECOMP* tables, void* r, const int64_t* a); -/** @brief frees a precomputed conversion data initialized with new_reim_from_znx32_precomp. */ -EXPORT REIM_FROM_ZNX64_PRECOMP* new_reim_from_znx64_precomp(uint32_t m, uint32_t maxbnd); -#define delete_reim_from_znx64_precomp free -EXPORT void reim_from_znx64_simple(uint32_t m, uint32_t log2bound, void* r, const int64_t* a); - -/** - * @brief prepares a conversion from TnX to the cplx layout. - * @param m the target complex dimension m from C[X] mod X^m-i. Note that the inputs have n=2m - * torus32 coefficients. The resulting precomputed data must - * be freed with `delete_reim_from_tnx32_precomp` - */ - -EXPORT REIM_FROM_TNX32_PRECOMP* new_reim_from_tnx32_precomp(uint32_t m); -/** - * @brief converts from TnX to the cplx layout. - * @param tables precomputed data obtained by new_reim_from_tnx32_precomp. - * @param r resulting array of m complexes coefficients mod X^m-i - * @param x input array of n torus32 coefficients mod X^n+1 - */ -EXPORT void reim_from_tnx32(const REIM_FROM_TNX32_PRECOMP* tables, void* r, const int32_t* a); -/** @brief frees a precomputed conversion data initialized with new_reim_from_tnx32_precomp. */ -#define delete_reim_from_tnx32_precomp free - -/** - * @brief prepares a rescale and conversion from the cplx layout to TnX. - * @param m the target complex dimension m from C[X] mod X^m-i. Note that the outputs have n=2m - * torus32 coefficients. - * @param divisor must be a power of two. The inputs are rescaled by divisor before being reduced modulo 1. - * Remember that the output of an iFFT must be divided by m. - * @param log2overhead all inputs absolute values must be within divisor.2^log2overhead. - * For any inputs outside of these bounds, the conversion is undefined behaviour. - * The maximum supported log2overhead is 52, and the algorithm is faster for log2overhead=18. - */ -EXPORT REIM_TO_TNX32_PRECOMP* new_reim_to_tnx32_precomp(uint32_t m, double divisor, uint32_t log2overhead); - -/** - * @brief rescale, converts and reduce mod 1 from cplx layout to torus32. - * @param tables precomputed data obtained by new_reim_from_tnx32_precomp. - * @param r resulting array of n torus32 coefficients mod X^n+1 - * @param x input array of m cplx coefficients mod X^m-i - */ -EXPORT void reim_to_tnx32(const REIM_TO_TNX32_PRECOMP* tables, int32_t* r, const void* a); -#define delete_reim_to_tnx32_precomp free - -/** - * @brief prepares a rescale and conversion from the cplx layout to TnX (doubles). - * @param m the target complex dimension m from C[X] mod X^m-i. Note that the outputs have n=2m - * torus32 coefficients. - * @param divisor must be a power of two. The inputs are rescaled by divisor before being reduced modulo 1. - * Remember that the output of an iFFT must be divided by m. - * @param log2overhead all inputs absolute values must be within divisor.2^log2overhead. - * For any inputs outside of these bounds, the conversion is undefined behaviour. - * The maximum supported log2overhead is 52, and the algorithm is faster for log2overhead=18. - */ -EXPORT REIM_TO_TNX_PRECOMP* new_reim_to_tnx_precomp(uint32_t m, double divisor, uint32_t log2overhead); -/** - * @brief rescale, converts and reduce mod 1 from cplx layout to torus32. - * @param tables precomputed data obtained by new_reim_from_tnx32_precomp. - * @param r resulting array of n torus32 coefficients mod X^n+1 - * @param x input array of m cplx coefficients mod X^m-i - */ -EXPORT void reim_to_tnx(const REIM_TO_TNX_PRECOMP* tables, double* r, const double* a); -#define delete_reim_to_tnx_precomp free -EXPORT void reim_to_tnx_simple(uint32_t m, double divisor, uint32_t log2overhead, double* r, const double* a); - -EXPORT REIM_TO_ZNX64_PRECOMP* new_reim_to_znx64_precomp(uint32_t m, double divisor, uint32_t log2bound); -#define delete_reim_to_znx64_precomp free -EXPORT void reim_to_znx64(const REIM_TO_ZNX64_PRECOMP* precomp, int64_t* r, const void* a); -EXPORT void reim_to_znx64_simple(uint32_t m, double divisor, uint32_t log2bound, int64_t* r, const void* a); - -/** - * @brief Simpler API for the fft function. - * For each dimension, the precomputed tables for this dimension are generated automatically. - * It is advised to do one dry-run per desired dimension before using in a multithread environment */ -EXPORT void reim_fft_simple(uint32_t m, void* data); -/** - * @brief Simpler API for the ifft function. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension in the main thread before using in a multithread - * environment */ -EXPORT void reim_ifft_simple(uint32_t m, void* data); -/** - * @brief Simpler API for the fftvec multiplication function. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void reim_fftvec_mul_simple(uint32_t m, void* r, const void* a, const void* b); -/** - * @brief Simpler API for the fftvec addmul function. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void reim_fftvec_addmul_simple(uint32_t m, void* r, const void* a, const void* b); -/** - * @brief Simpler API for the znx32 to cplx conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void reim_from_znx32_simple(uint32_t m, uint32_t log2bound, void* r, const int32_t* x); -/** - * @brief Simpler API for the tnx32 to cplx conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void reim_from_tnx32_simple(uint32_t m, void* r, const int32_t* x); -/** - * @brief Simpler API for the cplx to tnx32 conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void reim_to_tnx32_simple(uint32_t m, double divisor, uint32_t log2overhead, int32_t* r, const void* x); - -#endif // SPQLIOS_REIM_FFT_H
D spqlios/lib/spqlios/reim/reim_fft16_avx_fma.s

@@ -1,167 +0,0 @@

-#rdi datare ptr -#rsi dataim ptr -#rdx om ptr -.globl reim_fft16_avx_fma -reim_fft16_avx_fma: -vmovupd (%rdi),%ymm0 # ra0 -vmovupd 0x20(%rdi),%ymm1 # ra4 -vmovupd 0x40(%rdi),%ymm2 # ra8 -vmovupd 0x60(%rdi),%ymm3 # ra12 -vmovupd (%rsi),%ymm4 # ia0 -vmovupd 0x20(%rsi),%ymm5 # ia4 -vmovupd 0x40(%rsi),%ymm6 # ia8 -vmovupd 0x60(%rsi),%ymm7 # ia12 - -vmovupd (%rdx),%xmm12 -vinsertf128 $1, %xmm12, %ymm12, %ymm12 # omriri -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omai -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omar -vmulpd %ymm6,%ymm13,%ymm8 # ia0.omai -vmulpd %ymm7,%ymm13,%ymm9 # ia4.omai -vmulpd %ymm2,%ymm13,%ymm10 # ra0.omai -vmulpd %ymm3,%ymm13,%ymm11 # ra4.omai -vfmsub231pd %ymm2,%ymm12,%ymm8 # rprod0 -vfmsub231pd %ymm3,%ymm12,%ymm9 # rprod4 -vfmadd231pd %ymm6,%ymm12,%ymm10 # iprod0 -vfmadd231pd %ymm7,%ymm12,%ymm11 # iprod4 -vsubpd %ymm8,%ymm0,%ymm2 -vsubpd %ymm9,%ymm1,%ymm3 -vsubpd %ymm10,%ymm4,%ymm6 -vsubpd %ymm11,%ymm5,%ymm7 -vaddpd %ymm8,%ymm0,%ymm0 -vaddpd %ymm9,%ymm1,%ymm1 -vaddpd %ymm10,%ymm4,%ymm4 -vaddpd %ymm11,%ymm5,%ymm5 - -1: -vmovupd 16(%rdx),%xmm12 -vinsertf128 $1, %xmm12, %ymm12, %ymm12 # omriri -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omai -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omar -vmulpd %ymm5,%ymm13,%ymm8 # ia0.omai (tw) -vmulpd %ymm7,%ymm12,%ymm9 # ia4.omar (itw) -vmulpd %ymm1,%ymm13,%ymm10 # ra0.omai (tw) -vmulpd %ymm3,%ymm12,%ymm11 # ra4.omar (itw) -vfmsub231pd %ymm1,%ymm12,%ymm8 # rprod0 (tw) -vfmadd231pd %ymm3,%ymm13,%ymm9 # rprod4 (itw) -vfmadd231pd %ymm5,%ymm12,%ymm10 # iprod0 (tw) -vfmsub231pd %ymm7,%ymm13,%ymm11 # iprod4 (itw) -vsubpd %ymm8,%ymm0,%ymm1 -vaddpd %ymm9,%ymm2,%ymm3 -vsubpd %ymm10,%ymm4,%ymm5 -vaddpd %ymm11,%ymm6,%ymm7 -vaddpd %ymm8,%ymm0,%ymm0 -vsubpd %ymm9,%ymm2,%ymm2 -vaddpd %ymm10,%ymm4,%ymm4 -vsubpd %ymm11,%ymm6,%ymm6 - -2: -vmovupd 0x20(%rdx),%ymm12 -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omaiii'i' -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omarrr'r' - -vperm2f128 $0x31,%ymm2,%ymm0,%ymm8 # ymm8 contains re to mul (tw) -vperm2f128 $0x31,%ymm3,%ymm1,%ymm9 # ymm9 contains re to mul (itw) -vperm2f128 $0x31,%ymm6,%ymm4,%ymm10 # ymm10 contains im to mul (tw) -vperm2f128 $0x31,%ymm7,%ymm5,%ymm11 # ymm11 contains im to mul (itw) -vperm2f128 $0x20,%ymm2,%ymm0,%ymm0 # ymm0 contains re to add (tw) -vperm2f128 $0x20,%ymm3,%ymm1,%ymm1 # ymm1 contains re to add (itw) -vperm2f128 $0x20,%ymm6,%ymm4,%ymm2 # ymm2 contains im to add (tw) -vperm2f128 $0x20,%ymm7,%ymm5,%ymm3 # ymm3 contains im to add (itw) - -vmulpd %ymm10,%ymm13,%ymm4 # ia0.omai (tw) -vmulpd %ymm11,%ymm12,%ymm5 # ia4.omar (itw) -vmulpd %ymm8,%ymm13,%ymm6 # ra0.omai (tw) -vmulpd %ymm9,%ymm12,%ymm7 # ra4.omar (itw) -vfmsub231pd %ymm8,%ymm12,%ymm4 # rprod0 (tw) -vfmadd231pd %ymm9,%ymm13,%ymm5 # rprod4 (itw) -vfmadd231pd %ymm10,%ymm12,%ymm6 # iprod0 (tw) -vfmsub231pd %ymm11,%ymm13,%ymm7 # iprod4 (itw) -vsubpd %ymm4,%ymm0,%ymm8 -vaddpd %ymm5,%ymm1,%ymm9 -vsubpd %ymm6,%ymm2,%ymm10 -vaddpd %ymm7,%ymm3,%ymm11 -vaddpd %ymm4,%ymm0,%ymm0 -vsubpd %ymm5,%ymm1,%ymm1 -vaddpd %ymm6,%ymm2,%ymm2 -vsubpd %ymm7,%ymm3,%ymm3 - -#vperm2f128 $0x31,%ymm10,%ymm2,%ymm6 -#vperm2f128 $0x31,%ymm11,%ymm3,%ymm7 -#vperm2f128 $0x20,%ymm10,%ymm2,%ymm4 -#vperm2f128 $0x20,%ymm11,%ymm3,%ymm5 -#vperm2f128 $0x31,%ymm8,%ymm0,%ymm2 -#vperm2f128 $0x31,%ymm9,%ymm1,%ymm3 -#vperm2f128 $0x20,%ymm8,%ymm0,%ymm0 -#vperm2f128 $0x20,%ymm9,%ymm1,%ymm1 - -3: -vmovupd 0x40(%rdx),%ymm12 -vmovupd 0x60(%rdx),%ymm13 - -#vperm2f128 $0x31,%ymm2,%ymm0,%ymm8 # ymm8 contains re to mul (tw) -#vperm2f128 $0x31,%ymm3,%ymm1,%ymm9 # ymm9 contains re to mul (itw) -#vperm2f128 $0x31,%ymm6,%ymm4,%ymm10 # ymm10 contains im to mul (tw) -#vperm2f128 $0x31,%ymm7,%ymm5,%ymm11 # ymm11 contains im to mul (itw) -#vperm2f128 $0x20,%ymm2,%ymm0,%ymm0 # ymm0 contains re to add (tw) -#vperm2f128 $0x20,%ymm3,%ymm1,%ymm1 # ymm1 contains re to add (itw) -#vperm2f128 $0x20,%ymm6,%ymm4,%ymm2 # ymm2 contains im to add (tw) -#vperm2f128 $0x20,%ymm7,%ymm5,%ymm3 # ymm3 contains im to add (itw) - -vunpckhpd %ymm1,%ymm0,%ymm4 # (0,1) -> (0,4) -vunpckhpd %ymm3,%ymm2,%ymm6 # (2,3) -> (2,6) -vunpckhpd %ymm9,%ymm8,%ymm5 # (8,9) -> (1,5) -vunpckhpd %ymm11,%ymm10,%ymm7 # (10,11) -> (3,7) -vunpcklpd %ymm1,%ymm0,%ymm0 -vunpcklpd %ymm3,%ymm2,%ymm2 -vunpcklpd %ymm9,%ymm8,%ymm1 -vunpcklpd %ymm11,%ymm10,%ymm3 - -vmulpd %ymm6,%ymm13,%ymm8 # ia0.omai (tw) -vmulpd %ymm7,%ymm12,%ymm9 # ia4.omar (itw) -vmulpd %ymm4,%ymm13,%ymm10 # ra0.omai (tw) -vmulpd %ymm5,%ymm12,%ymm11 # ra4.omar (itw) -vfmsub231pd %ymm4,%ymm12,%ymm8 # rprod0 (tw) -vfmadd231pd %ymm5,%ymm13,%ymm9 # rprod4 (itw) -vfmadd231pd %ymm6,%ymm12,%ymm10 # iprod0 (tw) -vfmsub231pd %ymm7,%ymm13,%ymm11 # iprod4 (itw) -vsubpd %ymm8,%ymm0,%ymm4 -vaddpd %ymm9,%ymm1,%ymm5 -vsubpd %ymm10,%ymm2,%ymm6 -vaddpd %ymm11,%ymm3,%ymm7 -vaddpd %ymm8,%ymm0,%ymm0 -vsubpd %ymm9,%ymm1,%ymm1 -vaddpd %ymm10,%ymm2,%ymm2 -vsubpd %ymm11,%ymm3,%ymm3 - -vunpckhpd %ymm7,%ymm3,%ymm11 # (0,4) -> (0,1) -vunpckhpd %ymm5,%ymm1,%ymm9 # (2,6) -> (2,3) -vunpcklpd %ymm7,%ymm3,%ymm10 -vunpcklpd %ymm5,%ymm1,%ymm8 -vunpckhpd %ymm6,%ymm2,%ymm3 # (1,5) -> (8,9) -vunpckhpd %ymm4,%ymm0,%ymm1 # (3,7) -> (10,11) -vunpcklpd %ymm6,%ymm2,%ymm2 -vunpcklpd %ymm4,%ymm0,%ymm0 - -vperm2f128 $0x31,%ymm10,%ymm2,%ymm6 -vperm2f128 $0x31,%ymm11,%ymm3,%ymm7 -vperm2f128 $0x20,%ymm10,%ymm2,%ymm4 -vperm2f128 $0x20,%ymm11,%ymm3,%ymm5 -vperm2f128 $0x31,%ymm8,%ymm0,%ymm2 -vperm2f128 $0x31,%ymm9,%ymm1,%ymm3 -vperm2f128 $0x20,%ymm8,%ymm0,%ymm0 -vperm2f128 $0x20,%ymm9,%ymm1,%ymm1 - -4: -vmovupd %ymm0,(%rdi) # ra0 -vmovupd %ymm1,0x20(%rdi) # ra4 -vmovupd %ymm2,0x40(%rdi) # ra8 -vmovupd %ymm3,0x60(%rdi) # ra12 -vmovupd %ymm4,(%rsi) # ia0 -vmovupd %ymm5,0x20(%rsi) # ia4 -vmovupd %ymm6,0x40(%rsi) # ia8 -vmovupd %ymm7,0x60(%rsi) # ia12 -vzeroupper -ret -.size reim_fft16_avx_fma, .-reim_fft16_avx_fma -.section .note.GNU-stack,"",@progbits
D spqlios/lib/spqlios/reim/reim_fft16_avx_fma_win32.s

@@ -1,203 +0,0 @@

- .text - .p2align 4 - .globl reim_fft16_avx_fma - .def reim_fft16_avx_fma; .scl 2; .type 32; .endef -reim_fft16_avx_fma: - - pushq %rdi - pushq %rsi - movq %rcx,%rdi - movq %rdx,%rsi - movq %r8,%rdx - subq $0x100,%rsp - movdqu %xmm6,(%rsp) - movdqu %xmm7,0x10(%rsp) - movdqu %xmm8,0x20(%rsp) - movdqu %xmm9,0x30(%rsp) - movdqu %xmm10,0x40(%rsp) - movdqu %xmm11,0x50(%rsp) - movdqu %xmm12,0x60(%rsp) - movdqu %xmm13,0x70(%rsp) - movdqu %xmm14,0x80(%rsp) - movdqu %xmm15,0x90(%rsp) - callq reim_fft16_avx_fma_amd64 - movdqu (%rsp),%xmm6 - movdqu 0x10(%rsp),%xmm7 - movdqu 0x20(%rsp),%xmm8 - movdqu 0x30(%rsp),%xmm9 - movdqu 0x40(%rsp),%xmm10 - movdqu 0x50(%rsp),%xmm11 - movdqu 0x60(%rsp),%xmm12 - movdqu 0x70(%rsp),%xmm13 - movdqu 0x80(%rsp),%xmm14 - movdqu 0x90(%rsp),%xmm15 - addq $0x100,%rsp - popq %rsi - popq %rdi - retq - -#rdi datare ptr -#rsi dataim ptr -#rdx om ptr -#.globl reim_fft16_avx_fma_amd64 -reim_fft16_avx_fma_amd64: -vmovupd (%rdi),%ymm0 # ra0 -vmovupd 0x20(%rdi),%ymm1 # ra4 -vmovupd 0x40(%rdi),%ymm2 # ra8 -vmovupd 0x60(%rdi),%ymm3 # ra12 -vmovupd (%rsi),%ymm4 # ia0 -vmovupd 0x20(%rsi),%ymm5 # ia4 -vmovupd 0x40(%rsi),%ymm6 # ia8 -vmovupd 0x60(%rsi),%ymm7 # ia12 - -vmovupd (%rdx),%xmm12 -vinsertf128 $1, %xmm12, %ymm12, %ymm12 # omriri -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omai -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omar -vmulpd %ymm6,%ymm13,%ymm8 # ia0.omai -vmulpd %ymm7,%ymm13,%ymm9 # ia4.omai -vmulpd %ymm2,%ymm13,%ymm10 # ra0.omai -vmulpd %ymm3,%ymm13,%ymm11 # ra4.omai -vfmsub231pd %ymm2,%ymm12,%ymm8 # rprod0 -vfmsub231pd %ymm3,%ymm12,%ymm9 # rprod4 -vfmadd231pd %ymm6,%ymm12,%ymm10 # iprod0 -vfmadd231pd %ymm7,%ymm12,%ymm11 # iprod4 -vsubpd %ymm8,%ymm0,%ymm2 -vsubpd %ymm9,%ymm1,%ymm3 -vsubpd %ymm10,%ymm4,%ymm6 -vsubpd %ymm11,%ymm5,%ymm7 -vaddpd %ymm8,%ymm0,%ymm0 -vaddpd %ymm9,%ymm1,%ymm1 -vaddpd %ymm10,%ymm4,%ymm4 -vaddpd %ymm11,%ymm5,%ymm5 - -1: -vmovupd 16(%rdx),%xmm12 -vinsertf128 $1, %xmm12, %ymm12, %ymm12 # omriri -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omai -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omar -vmulpd %ymm5,%ymm13,%ymm8 # ia0.omai (tw) -vmulpd %ymm7,%ymm12,%ymm9 # ia4.omar (itw) -vmulpd %ymm1,%ymm13,%ymm10 # ra0.omai (tw) -vmulpd %ymm3,%ymm12,%ymm11 # ra4.omar (itw) -vfmsub231pd %ymm1,%ymm12,%ymm8 # rprod0 (tw) -vfmadd231pd %ymm3,%ymm13,%ymm9 # rprod4 (itw) -vfmadd231pd %ymm5,%ymm12,%ymm10 # iprod0 (tw) -vfmsub231pd %ymm7,%ymm13,%ymm11 # iprod4 (itw) -vsubpd %ymm8,%ymm0,%ymm1 -vaddpd %ymm9,%ymm2,%ymm3 -vsubpd %ymm10,%ymm4,%ymm5 -vaddpd %ymm11,%ymm6,%ymm7 -vaddpd %ymm8,%ymm0,%ymm0 -vsubpd %ymm9,%ymm2,%ymm2 -vaddpd %ymm10,%ymm4,%ymm4 -vsubpd %ymm11,%ymm6,%ymm6 - -2: -vmovupd 0x20(%rdx),%ymm12 -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omaiii'i' -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omarrr'r' - -vperm2f128 $0x31,%ymm2,%ymm0,%ymm8 # ymm8 contains re to mul (tw) -vperm2f128 $0x31,%ymm3,%ymm1,%ymm9 # ymm9 contains re to mul (itw) -vperm2f128 $0x31,%ymm6,%ymm4,%ymm10 # ymm10 contains im to mul (tw) -vperm2f128 $0x31,%ymm7,%ymm5,%ymm11 # ymm11 contains im to mul (itw) -vperm2f128 $0x20,%ymm2,%ymm0,%ymm0 # ymm0 contains re to add (tw) -vperm2f128 $0x20,%ymm3,%ymm1,%ymm1 # ymm1 contains re to add (itw) -vperm2f128 $0x20,%ymm6,%ymm4,%ymm2 # ymm2 contains im to add (tw) -vperm2f128 $0x20,%ymm7,%ymm5,%ymm3 # ymm3 contains im to add (itw) - -vmulpd %ymm10,%ymm13,%ymm4 # ia0.omai (tw) -vmulpd %ymm11,%ymm12,%ymm5 # ia4.omar (itw) -vmulpd %ymm8,%ymm13,%ymm6 # ra0.omai (tw) -vmulpd %ymm9,%ymm12,%ymm7 # ra4.omar (itw) -vfmsub231pd %ymm8,%ymm12,%ymm4 # rprod0 (tw) -vfmadd231pd %ymm9,%ymm13,%ymm5 # rprod4 (itw) -vfmadd231pd %ymm10,%ymm12,%ymm6 # iprod0 (tw) -vfmsub231pd %ymm11,%ymm13,%ymm7 # iprod4 (itw) -vsubpd %ymm4,%ymm0,%ymm8 -vaddpd %ymm5,%ymm1,%ymm9 -vsubpd %ymm6,%ymm2,%ymm10 -vaddpd %ymm7,%ymm3,%ymm11 -vaddpd %ymm4,%ymm0,%ymm0 -vsubpd %ymm5,%ymm1,%ymm1 -vaddpd %ymm6,%ymm2,%ymm2 -vsubpd %ymm7,%ymm3,%ymm3 - -#vperm2f128 $0x31,%ymm10,%ymm2,%ymm6 -#vperm2f128 $0x31,%ymm11,%ymm3,%ymm7 -#vperm2f128 $0x20,%ymm10,%ymm2,%ymm4 -#vperm2f128 $0x20,%ymm11,%ymm3,%ymm5 -#vperm2f128 $0x31,%ymm8,%ymm0,%ymm2 -#vperm2f128 $0x31,%ymm9,%ymm1,%ymm3 -#vperm2f128 $0x20,%ymm8,%ymm0,%ymm0 -#vperm2f128 $0x20,%ymm9,%ymm1,%ymm1 - -3: -vmovupd 0x40(%rdx),%ymm12 -vmovupd 0x60(%rdx),%ymm13 - -#vperm2f128 $0x31,%ymm2,%ymm0,%ymm8 # ymm8 contains re to mul (tw) -#vperm2f128 $0x31,%ymm3,%ymm1,%ymm9 # ymm9 contains re to mul (itw) -#vperm2f128 $0x31,%ymm6,%ymm4,%ymm10 # ymm10 contains im to mul (tw) -#vperm2f128 $0x31,%ymm7,%ymm5,%ymm11 # ymm11 contains im to mul (itw) -#vperm2f128 $0x20,%ymm2,%ymm0,%ymm0 # ymm0 contains re to add (tw) -#vperm2f128 $0x20,%ymm3,%ymm1,%ymm1 # ymm1 contains re to add (itw) -#vperm2f128 $0x20,%ymm6,%ymm4,%ymm2 # ymm2 contains im to add (tw) -#vperm2f128 $0x20,%ymm7,%ymm5,%ymm3 # ymm3 contains im to add (itw) - -vunpckhpd %ymm1,%ymm0,%ymm4 # (0,1) -> (0,4) -vunpckhpd %ymm3,%ymm2,%ymm6 # (2,3) -> (2,6) -vunpckhpd %ymm9,%ymm8,%ymm5 # (8,9) -> (1,5) -vunpckhpd %ymm11,%ymm10,%ymm7 # (10,11) -> (3,7) -vunpcklpd %ymm1,%ymm0,%ymm0 -vunpcklpd %ymm3,%ymm2,%ymm2 -vunpcklpd %ymm9,%ymm8,%ymm1 -vunpcklpd %ymm11,%ymm10,%ymm3 - -vmulpd %ymm6,%ymm13,%ymm8 # ia0.omai (tw) -vmulpd %ymm7,%ymm12,%ymm9 # ia4.omar (itw) -vmulpd %ymm4,%ymm13,%ymm10 # ra0.omai (tw) -vmulpd %ymm5,%ymm12,%ymm11 # ra4.omar (itw) -vfmsub231pd %ymm4,%ymm12,%ymm8 # rprod0 (tw) -vfmadd231pd %ymm5,%ymm13,%ymm9 # rprod4 (itw) -vfmadd231pd %ymm6,%ymm12,%ymm10 # iprod0 (tw) -vfmsub231pd %ymm7,%ymm13,%ymm11 # iprod4 (itw) -vsubpd %ymm8,%ymm0,%ymm4 -vaddpd %ymm9,%ymm1,%ymm5 -vsubpd %ymm10,%ymm2,%ymm6 -vaddpd %ymm11,%ymm3,%ymm7 -vaddpd %ymm8,%ymm0,%ymm0 -vsubpd %ymm9,%ymm1,%ymm1 -vaddpd %ymm10,%ymm2,%ymm2 -vsubpd %ymm11,%ymm3,%ymm3 - -vunpckhpd %ymm7,%ymm3,%ymm11 # (0,4) -> (0,1) -vunpckhpd %ymm5,%ymm1,%ymm9 # (2,6) -> (2,3) -vunpcklpd %ymm7,%ymm3,%ymm10 -vunpcklpd %ymm5,%ymm1,%ymm8 -vunpckhpd %ymm6,%ymm2,%ymm3 # (1,5) -> (8,9) -vunpckhpd %ymm4,%ymm0,%ymm1 # (3,7) -> (10,11) -vunpcklpd %ymm6,%ymm2,%ymm2 -vunpcklpd %ymm4,%ymm0,%ymm0 - -vperm2f128 $0x31,%ymm10,%ymm2,%ymm6 -vperm2f128 $0x31,%ymm11,%ymm3,%ymm7 -vperm2f128 $0x20,%ymm10,%ymm2,%ymm4 -vperm2f128 $0x20,%ymm11,%ymm3,%ymm5 -vperm2f128 $0x31,%ymm8,%ymm0,%ymm2 -vperm2f128 $0x31,%ymm9,%ymm1,%ymm3 -vperm2f128 $0x20,%ymm8,%ymm0,%ymm0 -vperm2f128 $0x20,%ymm9,%ymm1,%ymm1 - -4: -vmovupd %ymm0,(%rdi) # ra0 -vmovupd %ymm1,0x20(%rdi) # ra4 -vmovupd %ymm2,0x40(%rdi) # ra8 -vmovupd %ymm3,0x60(%rdi) # ra12 -vmovupd %ymm4,(%rsi) # ia0 -vmovupd %ymm5,0x20(%rsi) # ia4 -vmovupd %ymm6,0x40(%rsi) # ia8 -vmovupd %ymm7,0x60(%rsi) # ia12 -vzeroupper -ret
D spqlios/lib/spqlios/reim/reim_fft4_avx_fma.c

@@ -1,66 +0,0 @@

-#include <assert.h> -#include <immintrin.h> -#include <stdint.h> - -#include "reim_fft_private.h" - -__always_inline void reim_ctwiddle_avx_fma(__m128d* ra, __m128d* rb, __m128d* ia, __m128d* ib, const __m128d omre, - const __m128d omim) { - // rb * omre - ib * omim; - __m128d rprod0 = _mm_mul_pd(*ib, omim); - rprod0 = _mm_fmsub_pd(*rb, omre, rprod0); - - // rb * omim + ib * omre; - __m128d iprod0 = _mm_mul_pd(*rb, omim); - iprod0 = _mm_fmadd_pd(*ib, omre, iprod0); - - *rb = _mm_sub_pd(*ra, rprod0); - *ib = _mm_sub_pd(*ia, iprod0); - *ra = _mm_add_pd(*ra, rprod0); - *ia = _mm_add_pd(*ia, iprod0); -} - -EXPORT void reim_fft4_avx_fma(double* dre, double* dim, const void* ompv) { - const double* omp = (const double*)ompv; - - __m128d ra0 = _mm_loadu_pd(dre); - __m128d ra2 = _mm_loadu_pd(dre + 2); - __m128d ia0 = _mm_loadu_pd(dim); - __m128d ia2 = _mm_loadu_pd(dim + 2); - - // 1 - { - // duplicate omegas in precomp? - __m128d om = _mm_loadu_pd(omp); - __m128d omre = _mm_permute_pd(om, 0); - __m128d omim = _mm_permute_pd(om, 3); - - reim_ctwiddle_avx_fma(&ra0, &ra2, &ia0, &ia2, omre, omim); - } - - // 2 - { - const __m128d fft4neg = _mm_castsi128_pd(_mm_set_epi64x(UINT64_C(1) << 63, 0)); - __m128d om = _mm_loadu_pd(omp + 2); // om: r,i - __m128d omim = _mm_permute_pd(om, 1); // omim: i,r - __m128d omre = _mm_xor_pd(om, fft4neg); // omre: r,-i - - __m128d rb = _mm_unpackhi_pd(ra0, ra2); // (r0, r1), (r2, r3) -> (r1, r3) - __m128d ib = _mm_unpackhi_pd(ia0, ia2); // (i0, i1), (i2, i3) -> (i1, i3) - __m128d ra = _mm_unpacklo_pd(ra0, ra2); // (r0, r1), (r2, r3) -> (r0, r2) - __m128d ia = _mm_unpacklo_pd(ia0, ia2); // (i0, i1), (i2, i3) -> (i0, i2) - - reim_ctwiddle_avx_fma(&ra, &rb, &ia, &ib, omre, omim); - - ra0 = _mm_unpacklo_pd(ra, rb); - ia0 = _mm_unpacklo_pd(ia, ib); - ra2 = _mm_unpackhi_pd(ra, rb); - ia2 = _mm_unpackhi_pd(ia, ib); - } - - // 4 - _mm_storeu_pd(dre, ra0); - _mm_storeu_pd(dre + 2, ra2); - _mm_storeu_pd(dim, ia0); - _mm_storeu_pd(dim + 2, ia2); -}
D spqlios/lib/spqlios/reim/reim_fft8_avx_fma.c

@@ -1,89 +0,0 @@

-#include <assert.h> -#include <immintrin.h> -#include <stdint.h> - -#include "reim_fft_private.h" - -__always_inline void reim_ctwiddle_avx_fma(__m256d* ra, __m256d* rb, __m256d* ia, __m256d* ib, const __m256d omre, - const __m256d omim) { - // rb * omre - ib * omim; - __m256d rprod0 = _mm256_mul_pd(*ib, omim); - rprod0 = _mm256_fmsub_pd(*rb, omre, rprod0); - - // rb * omim + ib * omre; - __m256d iprod0 = _mm256_mul_pd(*rb, omim); - iprod0 = _mm256_fmadd_pd(*ib, omre, iprod0); - - *rb = _mm256_sub_pd(*ra, rprod0); - *ib = _mm256_sub_pd(*ia, iprod0); - *ra = _mm256_add_pd(*ra, rprod0); - *ia = _mm256_add_pd(*ia, iprod0); -} - -EXPORT void reim_fft8_avx_fma(double* dre, double* dim, const void* ompv) { - const double* omp = (const double*)ompv; - - __m256d ra0 = _mm256_loadu_pd(dre); - __m256d ra4 = _mm256_loadu_pd(dre + 4); - __m256d ia0 = _mm256_loadu_pd(dim); - __m256d ia4 = _mm256_loadu_pd(dim + 4); - - // 1 - { - // duplicate omegas in precomp? - __m128d omri = _mm_loadu_pd(omp); - __m256d omriri = _mm256_set_m128d(omri, omri); - __m256d omi = _mm256_permute_pd(omriri, 15); - __m256d omr = _mm256_permute_pd(omriri, 0); - - reim_ctwiddle_avx_fma(&ra0, &ra4, &ia0, &ia4, omr, omi); - } - - // 2 - { - const __m128d fft8neg = _mm_castsi128_pd(_mm_set_epi64x(UINT64_C(1) << 63, 0)); - __m128d omri = _mm_loadu_pd(omp + 2); // r,i - __m128d omrmi = _mm_xor_pd(omri, fft8neg); // r,-i - __m256d omrirmi = _mm256_set_m128d(omrmi, omri); // r,i,r,-i - __m256d omi = _mm256_permute_pd(omrirmi, 3); // i,i,r,r - __m256d omr = _mm256_permute_pd(omrirmi, 12); // r,r,-i,-i - - __m256d rb = _mm256_permute2f128_pd(ra0, ra4, 0x31); - __m256d ib = _mm256_permute2f128_pd(ia0, ia4, 0x31); - __m256d ra = _mm256_permute2f128_pd(ra0, ra4, 0x20); - __m256d ia = _mm256_permute2f128_pd(ia0, ia4, 0x20); - - reim_ctwiddle_avx_fma(&ra, &rb, &ia, &ib, omr, omi); - - ra0 = _mm256_permute2f128_pd(ra, rb, 0x20); - ra4 = _mm256_permute2f128_pd(ra, rb, 0x31); - ia0 = _mm256_permute2f128_pd(ia, ib, 0x20); - ia4 = _mm256_permute2f128_pd(ia, ib, 0x31); - } - - // 3 - { - const __m256d fft8neg2 = _mm256_castsi256_pd(_mm256_set_epi64x(UINT64_C(1) << 63, UINT64_C(1) << 63, 0, 0)); - __m256d om = _mm256_loadu_pd(omp + 4); // r0,r1,i0,i1 - __m256d omi = _mm256_permute2f128_pd(om, om, 1); // i0,i1,r0,r1 - __m256d omr = _mm256_xor_pd(om, fft8neg2); // r0,r1,-i0,-i1 - - __m256d rb = _mm256_unpackhi_pd(ra0, ra4); - __m256d ib = _mm256_unpackhi_pd(ia0, ia4); - __m256d ra = _mm256_unpacklo_pd(ra0, ra4); - __m256d ia = _mm256_unpacklo_pd(ia0, ia4); - - reim_ctwiddle_avx_fma(&ra, &rb, &ia, &ib, omr, omi); - - ra4 = _mm256_unpackhi_pd(ra, rb); - ia4 = _mm256_unpackhi_pd(ia, ib); - ra0 = _mm256_unpacklo_pd(ra, rb); - ia0 = _mm256_unpacklo_pd(ia, ib); - } - - // 4 - _mm256_storeu_pd(dre, ra0); - _mm256_storeu_pd(dre + 4, ra4); - _mm256_storeu_pd(dim, ia0); - _mm256_storeu_pd(dim + 4, ia4); -}
D spqlios/lib/spqlios/reim/reim_fft_avx2.c

@@ -1,162 +0,0 @@

-#include "immintrin.h" -#include "reim_fft_internal.h" -#include "reim_fft_private.h" - -__always_inline void reim_twiddle_fft_avx2_fma(uint32_t h, double* re, double* im, double om[2]) { - const __m128d omx = _mm_load_pd(om); - const __m256d omra = _mm256_set_m128d(omx, omx); - const __m256d omi = _mm256_unpackhi_pd(omra, omra); - const __m256d omr = _mm256_unpacklo_pd(omra, omra); - double* r0 = re; - double* r1 = re + h; - double* i0 = im; - double* i1 = im + h; - for (uint32_t i=0; i<h; i+=4) { - __m256d ur0 = _mm256_loadu_pd(r0+i); - __m256d ur1 = _mm256_loadu_pd(r1+i); - __m256d ui0 = _mm256_loadu_pd(i0+i); - __m256d ui1 = _mm256_loadu_pd(i1+i); - __m256d tra = _mm256_mul_pd(omi, ui1); - __m256d tia = _mm256_mul_pd(omi, ur1); - tra = _mm256_fmsub_pd(omr, ur1, tra); - tia = _mm256_fmadd_pd(omr, ui1, tia); - ur1 = _mm256_sub_pd(ur0, tra); - ui1 = _mm256_sub_pd(ui0, tia); - ur0 = _mm256_add_pd(ur0, tra); - ui0 = _mm256_add_pd(ui0, tia); - _mm256_storeu_pd(r0+i, ur0); - _mm256_storeu_pd(r1+i, ur1); - _mm256_storeu_pd(i0+i, ui0); - _mm256_storeu_pd(i1+i, ui1); - } -} - -__always_inline void reim_bitwiddle_fft_avx2_fma(uint32_t h, double* re, double* im, double om[4]) { - double* const r0 = re; - double* const r1 = re + h; - double* const r2 = re + 2*h; - double* const r3 = re + 3*h; - double* const i0 = im; - double* const i1 = im + h; - double* const i2 = im + 2*h; - double* const i3 = im + 3*h; - const __m256d om0 = _mm256_loadu_pd(om); - const __m256d omb = _mm256_permute2f128_pd(om0,om0,0x11); - const __m256d oma = _mm256_permute2f128_pd(om0,om0,0x00); - const __m256d omai = _mm256_unpackhi_pd(oma, oma); - const __m256d omar = _mm256_unpacklo_pd(oma, oma); - const __m256d ombi = _mm256_unpackhi_pd(omb, omb); - const __m256d ombr = _mm256_unpacklo_pd(omb, omb); - for (uint32_t i=0; i<h; i+=4) { - __m256d ur0 = _mm256_loadu_pd(r0+i); - __m256d ur1 = _mm256_loadu_pd(r1+i); - __m256d ur2 = _mm256_loadu_pd(r2+i); - __m256d ur3 = _mm256_loadu_pd(r3+i); - __m256d ui0 = _mm256_loadu_pd(i0+i); - __m256d ui1 = _mm256_loadu_pd(i1+i); - __m256d ui2 = _mm256_loadu_pd(i2+i); - __m256d ui3 = _mm256_loadu_pd(i3+i); - //------ twiddles 1 - __m256d tra = _mm256_mul_pd(omai, ui2); - __m256d trb = _mm256_mul_pd(omai, ui3); - __m256d tia = _mm256_mul_pd(omai, ur2); - __m256d tib = _mm256_mul_pd(omai, ur3); - tra = _mm256_fmsub_pd(omar, ur2, tra); - trb = _mm256_fmsub_pd(omar, ur3, trb); - tia = _mm256_fmadd_pd(omar, ui2, tia); - tib = _mm256_fmadd_pd(omar, ui3, tib); - ur2 = _mm256_sub_pd(ur0, tra); - ur3 = _mm256_sub_pd(ur1, trb); - ui2 = _mm256_sub_pd(ui0, tia); - ui3 = _mm256_sub_pd(ui1, tib); - ur0 = _mm256_add_pd(ur0, tra); - ur1 = _mm256_add_pd(ur1, trb); - ui0 = _mm256_add_pd(ui0, tia); - ui1 = _mm256_add_pd(ui1, tib); - //------ twiddles 1 - tra = _mm256_mul_pd(ombi, ui1); - trb = _mm256_mul_pd(ombr, ui3); //ii - tia = _mm256_mul_pd(ombi, ur1); - tib = _mm256_mul_pd(ombr, ur3); //ri - tra = _mm256_fmsub_pd(ombr, ur1, tra); - trb = _mm256_fmadd_pd(ombi, ur3, trb); //-rr+ii - tia = _mm256_fmadd_pd(ombr, ui1, tia); - tib = _mm256_fmsub_pd(ombi, ui3, tib); //-ir-ri - ur1 = _mm256_sub_pd(ur0, tra); - ur3 = _mm256_add_pd(ur2, trb); - ui1 = _mm256_sub_pd(ui0, tia); - ui3 = _mm256_add_pd(ui2, tib); - ur0 = _mm256_add_pd(ur0, tra); - ur2 = _mm256_sub_pd(ur2, trb); - ui0 = _mm256_add_pd(ui0, tia); - ui2 = _mm256_sub_pd(ui2, tib); - ///--- - _mm256_storeu_pd(r0+i, ur0); - _mm256_storeu_pd(r1+i, ur1); - _mm256_storeu_pd(r2+i, ur2); - _mm256_storeu_pd(r3+i, ur3); - _mm256_storeu_pd(i0+i, ui0); - _mm256_storeu_pd(i1+i, ui1); - _mm256_storeu_pd(i2+i, ui2); - _mm256_storeu_pd(i3+i, ui3); - } -} - -void reim_fft_bfs_16_avx2_fma(uint32_t m, double* re, double* im, double** omg) { - uint32_t log2m = _mm_popcnt_u32(m-1); //log2(m); - uint32_t mm = m; - if ((log2m & 1) != 0) { - uint32_t h = mm >> 1; - // do the first twiddle iteration normally - reim_twiddle_fft_avx2_fma(h, re, im, *omg); - *omg += 2; - mm = h; - } - while (mm > 16) { - uint32_t h = mm >> 2; - for (uint32_t off = 0; off < m; off += mm) { - reim_bitwiddle_fft_avx2_fma(h, re + off, im + off, *omg); - *omg += 4; - } - mm = h; - } - if (mm!=16) abort(); // bug! - for (uint32_t off = 0; off < m; off += 16) { - reim_fft16_avx_fma(re+off, im+off, *omg); - *omg += 16; - } -} - -void reim_fft_rec_16_avx2_fma(uint32_t m, double* re, double* im, double** omg) { - if (m <= 2048) return reim_fft_bfs_16_avx2_fma(m, re, im, omg); - const uint32_t h = m / 2; - reim_twiddle_fft_avx2_fma(h, re, im, *omg); - *omg += 2; - reim_fft_rec_16_avx2_fma(h, re, im, omg); - reim_fft_rec_16_avx2_fma(h, re + h, im + h, omg); -} - -void reim_fft_avx2_fma(const REIM_FFT_PRECOMP* precomp, double* dat) { - const int32_t m = precomp->m; - double* omg = precomp->powomegas; - double* re = dat; - double* im = dat+m; - if (m <= 16) { - switch (m) { - case 1: - return; - case 2: - return reim_fft2_ref(re, im, omg); - case 4: - return reim_fft4_avx_fma(re, im, omg); - case 8: - return reim_fft8_avx_fma(re, im, omg); - case 16: - return reim_fft16_avx_fma(re, im, omg); - default: - abort(); // m is not a power of 2 - } - } - if (m <= 2048) return reim_fft_bfs_16_avx2_fma(m, re, im, &omg); - return reim_fft_rec_16_avx2_fma(m, re, im, &omg); -}
D spqlios/lib/spqlios/reim/reim_fft_core_template.h

@@ -1,162 +0,0 @@

-#ifndef SPQLIOS_REIM_FFT_CORE_TEMPLATE_H -#define SPQLIOS_REIM_FFT_CORE_TEMPLATE_H - -// this file contains the main template for the fft strategy. -// it is meant to be included once for each specialization (ref, avx, neon) -// all the leaf functions it uses shall be defined in the following macros, -// before including this header. - -#if !defined(reim_fft16_f) || !defined(reim_fft16_pom_offset) || !defined(fill_reim_fft16_omegas_f) -#error "missing reim16 definitions" -#endif -#if !defined(reim_fft8_f) || !defined(reim_fft8_pom_offset) || !defined(fill_reim_fft8_omegas_f) -#error "missing reim8 definitions" -#endif -#if !defined(reim_fft4_f) || !defined(reim_fft4_pom_offset) || !defined(fill_reim_fft4_omegas_f) -#error "missing reim4 definitions" -#endif -#if !defined(reim_fft2_f) || !defined(reim_fft2_pom_offset) || !defined(fill_reim_fft2_omegas_f) -#error "missing reim2 definitions" -#endif -#if !defined(reim_twiddle_fft_f) || !defined(reim_twiddle_fft_pom_offset) || !defined(fill_reim_twiddle_fft_omegas_f) -#error "missing twiddle definitions" -#endif -#if !defined(reim_bitwiddle_fft_f) || !defined(reim_bitwiddle_fft_pom_offset) || \ - !defined(fill_reim_bitwiddle_fft_omegas_f) -#error "missing bitwiddle definitions" -#endif -#if !defined(reim_fft_bfs_16_f) || !defined(fill_reim_fft_bfs_16_omegas_f) -#error "missing bfs_16 definitions" -#endif -#if !defined(reim_fft_rec_16_f) || !defined(fill_reim_fft_rec_16_omegas_f) -#error "missing rec_16 definitions" -#endif -#if !defined(reim_fft_f) || !defined(fill_reim_fft_omegas_f) -#error "missing main definitions" -#endif - -void reim_fft_bfs_16_f(uint64_t m, double* re, double* im, double** omg) { - uint64_t log2m = log2(m); - uint64_t mm = m; - if (log2m & 1) { - uint64_t h = mm >> 1; - // do the first twiddle iteration normally - reim_twiddle_fft_f(h, re, im, *omg); - *omg += reim_twiddle_fft_pom_offset; - mm = h; - } - while (mm > 16) { - uint64_t h = mm >> 2; - for (uint64_t off = 0; off < m; off += mm) { - reim_bitwiddle_fft_f(h, re + off, im + off, *omg); - *omg += reim_bitwiddle_fft_pom_offset; - } - mm = h; - } - if (mm != 16) abort(); // bug! - for (uint64_t off = 0; off < m; off += 16) { - reim_fft16_f(re + off, im + off, *omg); - *omg += reim_fft16_pom_offset; - } -} - -void fill_reim_fft_bfs_16_omegas_f(uint64_t m, double entry_pwr, double** omg) { - uint64_t log2m = log2(m); - uint64_t mm = m; - double ss = entry_pwr; - if (log2m % 2 != 0) { - uint64_t h = mm >> 1; - double s = ss / 2.; - // do the first twiddle iteration normally - fill_reim_twiddle_fft_omegas_f(s, omg); - mm = h; - ss = s; - } - while (mm > 16) { - uint64_t h = mm >> 2; - double s = ss / 4.; - for (uint64_t off = 0; off < m; off += mm) { - double rs0 = s + fracrevbits(off / mm) / 4.; - fill_reim_bitwiddle_fft_omegas_f(rs0, omg); - } - mm = h; - ss = s; - } - if (mm != 16) abort(); // bug! - for (uint64_t off = 0; off < m; off += 16) { - double s = ss + fracrevbits(off / 16); - fill_reim_fft16_omegas_f(s, omg); - } -} - -void reim_fft_rec_16_f(uint64_t m, double* re, double* im, double** omg) { - if (m <= 2048) return reim_fft_bfs_16_f(m, re, im, omg); - const uint32_t h = m >> 1; - reim_twiddle_fft_f(h, re, im, *omg); - *omg += reim_twiddle_fft_pom_offset; - reim_fft_rec_16_f(h, re, im, omg); - reim_fft_rec_16_f(h, re + h, im + h, omg); -} - -void fill_reim_fft_rec_16_omegas_f(uint64_t m, double entry_pwr, double** omg) { - if (m <= 2048) return fill_reim_fft_bfs_16_omegas_f(m, entry_pwr, omg); - const uint64_t h = m / 2; - const double s = entry_pwr / 2; - fill_reim_twiddle_fft_omegas_f(s, omg); - fill_reim_fft_rec_16_omegas_f(h, s, omg); - fill_reim_fft_rec_16_omegas_f(h, s + 0.5, omg); -} - -void reim_fft_f(const REIM_FFT_PRECOMP* precomp, double* dat) { - const int32_t m = precomp->m; - double* omg = precomp->powomegas; - double* re = dat; - double* im = dat + m; - if (m <= 16) { - switch (m) { - case 1: - return; - case 2: - return reim_fft2_f(re, im, omg); - case 4: - return reim_fft4_f(re, im, omg); - case 8: - return reim_fft8_f(re, im, omg); - case 16: - return reim_fft16_f(re, im, omg); - default: - abort(); // m is not a power of 2 - } - } - if (m <= 2048) return reim_fft_bfs_16_f(m, re, im, &omg); - return reim_fft_rec_16_f(m, re, im, &omg); -} - -void fill_reim_fft_omegas_f(uint64_t m, double entry_pwr, double** omg) { - if (m <= 16) { - switch (m) { - case 1: - break; - case 2: - fill_reim_fft2_omegas_f(entry_pwr, omg); - break; - case 4: - fill_reim_fft4_omegas_f(entry_pwr, omg); - break; - case 8: - fill_reim_fft8_omegas_f(entry_pwr, omg); - break; - case 16: - fill_reim_fft16_omegas_f(entry_pwr, omg); - break; - default: - abort(); // m is not a power of 2 - } - } else if (m <= 2048) { - fill_reim_fft_bfs_16_omegas_f(m, entry_pwr, omg); - } else { - fill_reim_fft_rec_16_omegas_f(m, entry_pwr, omg); - } -} - -#endif // SPQLIOS_REIM_FFT_CORE_TEMPLATE_H
D spqlios/lib/spqlios/reim/reim_fft_ifft.c

@@ -1,37 +0,0 @@

-#include <math.h> -#include <stdint.h> -#include <stdlib.h> -#include <string.h> - -#include "../commons_private.h" -#include "reim_fft_internal.h" -#include "reim_fft_private.h" - -double accurate_cos(int32_t i, int32_t n) { // cos(2pi*i/n) - i = ((i % n) + n) % n; - if (i >= 3 * n / 4) return cos(2. * M_PI * (n - i) / (double)(n)); - if (i >= 2 * n / 4) return -cos(2. * M_PI * (i - n / 2) / (double)(n)); - if (i >= 1 * n / 4) return -cos(2. * M_PI * (n / 2 - i) / (double)(n)); - return cos(2. * M_PI * (i) / (double)(n)); -} - -double accurate_sin(int32_t i, int32_t n) { // sin(2pi*i/n) - i = ((i % n) + n) % n; - if (i >= 3 * n / 4) return -sin(2. * M_PI * (n - i) / (double)(n)); - if (i >= 2 * n / 4) return -sin(2. * M_PI * (i - n / 2) / (double)(n)); - if (i >= 1 * n / 4) return sin(2. * M_PI * (n / 2 - i) / (double)(n)); - return sin(2. * M_PI * (i) / (double)(n)); -} - - -EXPORT double* reim_ifft_precomp_get_buffer(const REIM_IFFT_PRECOMP* tables, uint32_t buffer_index) { - return (double*)((uint8_t*) tables->aligned_buffers + buffer_index * tables->buf_size); -} - -EXPORT double* reim_fft_precomp_get_buffer(const REIM_FFT_PRECOMP* tables, uint32_t buffer_index) { - return (double*)((uint8_t*) tables->aligned_buffers + buffer_index * tables->buf_size); -} - - -EXPORT void reim_fft(const REIM_FFT_PRECOMP* tables, double* data) { tables->function(tables, data); } -EXPORT void reim_ifft(const REIM_IFFT_PRECOMP* tables, double* data) { tables->function(tables, data); }
D spqlios/lib/spqlios/reim/reim_fft_internal.h

@@ -1,143 +0,0 @@

-#ifndef SPQLIOS_REIM_FFT_INTERNAL_H -#define SPQLIOS_REIM_FFT_INTERNAL_H - -#include <math.h> -#include <stdint.h> -#include <stdlib.h> - -#include "reim_fft.h" - -EXPORT void reim_fftvec_addmul_fma(const REIM_FFTVEC_ADDMUL_PRECOMP* precomp, double* r, const double* a, - const double* b); -EXPORT void reim_fftvec_addmul_ref(const REIM_FFTVEC_ADDMUL_PRECOMP* precomp, double* r, const double* a, - const double* b); - -EXPORT void reim_fftvec_mul_fma(const REIM_FFTVEC_MUL_PRECOMP* tables, double* r, const double* a, const double* b); -EXPORT void reim_fftvec_mul_ref(const REIM_FFTVEC_MUL_PRECOMP* tables, double* r, const double* a, const double* b); - - -/** @brief r = x from ZnX (coeffs as signed int32_t's ) to double */ -EXPORT void reim_from_znx32_ref(const REIM_FROM_ZNX32_PRECOMP* precomp, void* r, const int32_t* x); -EXPORT void reim_from_znx32_avx2_fma(const REIM_FROM_ZNX32_PRECOMP* precomp, void* r, const int32_t* x); - -/** @brief r = x mod 1 (coeffs as double) */ -EXPORT void reim_to_tnx_ref(const REIM_TO_TNX_PRECOMP* tables, double* r, const double* x); -EXPORT void reim_to_tnx_avx(const REIM_TO_TNX_PRECOMP* tables, double* r, const double* x); - -EXPORT void reim_from_znx64_ref(const REIM_FROM_ZNX64_PRECOMP* precomp, void* r, const int64_t* x); -EXPORT void reim_from_znx64_bnd50_fma(const REIM_FROM_ZNX64_PRECOMP* precomp, void* r, const int64_t* x); - -EXPORT void reim_to_znx64_ref(const REIM_TO_ZNX64_PRECOMP* precomp, int64_t* r, const void* x); -EXPORT void reim_to_znx64_avx2_bnd63_fma(const REIM_TO_ZNX64_PRECOMP* precomp, int64_t* r, const void* x); -EXPORT void reim_to_znx64_avx2_bnd50_fma(const REIM_TO_ZNX64_PRECOMP* precomp, int64_t* r, const void* x); - -/** - * @brief compute the fft evaluations of P in place - * fft(data) = fft_rec(data, i); - * function fft_rec(data, omega) { - * if #data = 1: return data - * let s = sqrt(omega) w. re(s)>0 - * let (u,v) = merge_fft(data, s) - * return [fft_rec(u, s), fft_rec(v, -s)] - * } - * @param tables precomputed tables (contains all the powers of omega in the order they are used) - * @param data vector of m complexes (coeffs as input, evals as output) - */ -EXPORT void reim_fft_ref(const REIM_FFT_PRECOMP* tables, double* data); -EXPORT void reim_fft_avx2_fma(const REIM_FFT_PRECOMP* tables, double* data); - -/** - * @brief compute the ifft evaluations of P in place - * ifft(data) = ifft_rec(data, i); - * function ifft_rec(data, omega) { - * if #data = 1: return data - * let s = sqrt(omega) w. re(s)>0 - * let (u,v) = data - * return split_fft([ifft_rec(u, s), ifft_rec(v, -s)],s) - * } - * @param itables precomputed tables (contains all the powers of omega in the order they are used) - * @param data vector of m complexes (coeffs as input, evals as output) - */ -EXPORT void reim_ifft_ref(const REIM_IFFT_PRECOMP* itables, double* data); -EXPORT void reim_ifft_avx2_fma(const REIM_IFFT_PRECOMP* itables, double* data); - -/// new compressed implementation - -/** @brief naive FFT code mod X^m-exp(2i.pi.entry_pwr) */ -EXPORT void reim_naive_fft(uint64_t m, double entry_pwr, double* re, double* im); - -/** @brief 16-dimensional FFT with precomputed omegas */ -EXPORT void reim_fft16_neon(double* dre, double* dim, const void* omega); -EXPORT void reim_fft16_avx_fma(double* dre, double* dim, const void* omega); -EXPORT void reim_fft16_ref(double* dre, double* dim, const void* omega); - -/** @brief precompute omegas so that reim_fft16 functions */ -EXPORT void fill_reim_fft16_omegas(const double entry_pwr, double** omg); -EXPORT void fill_reim_fft16_omegas_neon(const double entry_pwr, double** omg); - -/** @brief 8-dimensional FFT with precomputed omegas */ -EXPORT void reim_fft8_avx_fma(double* dre, double* dim, const void* omega); -EXPORT void reim_fft8_ref(double* dre, double* dim, const void* omega); - -/** @brief precompute omegas so that reim_fft8 functions */ -EXPORT void fill_reim_fft8_omegas(const double entry_pwr, double** omg); - -/** @brief 4-dimensional FFT with precomputed omegas */ -EXPORT void reim_fft4_avx_fma(double* dre, double* dim, const void* omega); -EXPORT void reim_fft4_ref(double* dre, double* dim, const void* omega); - -/** @brief precompute omegas so that reim_fft8 functions */ -EXPORT void fill_reim_fft4_omegas(const double entry_pwr, double** omg); - -/** @brief 2-dimensional FFT with precomputed omegas */ -//EXPORT void reim_fft4_avx_fma(double* dre, double* dim, const void* omega); -EXPORT void reim_fft2_ref(double* dre, double* dim, const void* omega); - -/** @brief precompute omegas so that reim_fft8 functions */ -EXPORT void fill_reim_fft2_omegas(const double entry_pwr, double** omg); - -EXPORT void reim_fft_bfs_16_ref(uint64_t m, double* re, double* im, double** omg); -EXPORT void fill_reim_fft_bfs_16_omegas(uint64_t m, double entry_pwr, double** omg); - -EXPORT void reim_fft_rec_16_ref(uint64_t m, double* re, double* im, double** omg); -EXPORT void fill_reim_fft_rec_16_omegas(uint64_t m, double entry_pwr, double** omg); - - -/** @brief naive FFT code mod X^m-exp(2i.pi.entry_pwr) */ -EXPORT void reim_naive_ifft(uint64_t m, double entry_pwr, double* re, double* im); - -/** @brief 16-dimensional FFT with precomputed omegas */ -EXPORT void reim_ifft16_avx_fma(double* dre, double* dim, const void* omega); -EXPORT void reim_ifft16_ref(double* dre, double* dim, const void* omega); - -/** @brief precompute omegas so that reim_fft16 functions */ -EXPORT void fill_reim_ifft16_omegas(const double entry_pwr, double** omg); - -/** @brief 8-dimensional FFT with precomputed omegas */ -EXPORT void reim_ifft8_avx_fma(double* dre, double* dim, const void* omega); -EXPORT void reim_ifft8_ref(double* dre, double* dim, const void* omega); - -/** @brief precompute omegas so that reim_fft8 functions */ -EXPORT void fill_reim_ifft8_omegas(const double entry_pwr, double** omg); - -/** @brief 4-dimensional FFT with precomputed omegas */ -EXPORT void reim_ifft4_avx_fma(double* dre, double* dim, const void* omega); -EXPORT void reim_ifft4_ref(double* dre, double* dim, const void* omega); - -/** @brief precompute omegas so that reim_fft8 functions */ -EXPORT void fill_reim_ifft4_omegas(const double entry_pwr, double** omg); - -/** @brief 2-dimensional FFT with precomputed omegas */ -//EXPORT void reim_ifft2_avx_fma(double* dre, double* dim, const void* omega); -EXPORT void reim_ifft2_ref(double* dre, double* dim, const void* omega); - -/** @brief precompute omegas so that reim_fft8 functions */ -EXPORT void fill_reim_ifft2_omegas(const double entry_pwr, double** omg); - -EXPORT void reim_ifft_bfs_16_ref(uint64_t m, double* re, double* im, double** omg); -EXPORT void fill_reim_ifft_bfs_16_omegas(uint64_t m, double entry_pwr, double** omg); - -EXPORT void reim_ifft_rec_16_ref(uint64_t m, double* re, double* im, double** omg); -EXPORT void fill_reim_ifft_rec_16_omegas(uint64_t m, double entry_pwr, double** omg); - -#endif // SPQLIOS_REIM_FFT_INTERNAL_H
D spqlios/lib/spqlios/reim/reim_fft_neon.c

@@ -1,1627 +0,0 @@

-/* - * This file is adapted from the implementation of the FFT on Arm64/Neon - * available in https://github.com/cothan/Falcon-Arm (neon/fft.c). - * ============================================================================= - * Copyright (c) 2022 by Cryptographic Engineering Research Group (CERG) - * ECE Department, George Mason University - * Fairfax, VA, U.S.A. - * @author: Duc Tri Nguyen dnguye69@gmu.edu, cothannguyen@gmail.com - * Licensed under the Apache License, Version 2.0 (the "License"); - * ============================================================================= - * - * The original source file has been modified by the authors of spqlios-arithmetic - * to be interfaced with the dynamic twiddle factors generator of the spqlios-arithmetic - * library, as well as the recursive dfs strategy for large complex dimensions m>=2048. - */ - -#include <math.h> - -#include "../commons.h" -#include "../ext/neon_accel/macrof.h" -#include "../ext/neon_accel/macrofx4.h" - -void fill_reim_fft16_omegas_neon(const double entry_pwr, double** omg) { - const double j_pow = 1. / 8.; - const double k_pow = 1. / 16.; - const double pin = entry_pwr / 2.; - const double pin_2 = entry_pwr / 4.; - const double pin_4 = entry_pwr / 8.; - const double pin_8 = entry_pwr / 16.; - // 0 and 1 are real and imag of om - (*omg)[0] = cos(2. * M_PI * pin); - (*omg)[1] = sin(2. * M_PI * pin); - // 2 and 3 are real and imag of om^1/2 - (*omg)[2] = cos(2. * M_PI * (pin_2)); - (*omg)[3] = sin(2. * M_PI * (pin_2)); - // (4,5) and (6,7) are real and imag of om^1/4 and j.om^1/4 - (*omg)[4] = cos(2. * M_PI * (pin_4)); - (*omg)[5] = sin(2. * M_PI * (pin_4)); - (*omg)[6] = cos(2. * M_PI * (pin_4 + j_pow)); - (*omg)[7] = sin(2. * M_PI * (pin_4 + j_pow)); - // ((8,9,10,11),(12,13,14,15)) are 4 reals then 4 imag of om^1/8*(1,k,j,kj) - (*omg)[8] = cos(2. * M_PI * (pin_8)); - (*omg)[10] = cos(2. * M_PI * (pin_8 + j_pow)); - (*omg)[12] = cos(2. * M_PI * (pin_8 + k_pow)); - (*omg)[14] = cos(2. * M_PI * (pin_8 + j_pow + k_pow)); - (*omg)[9] = sin(2. * M_PI * (pin_8)); - (*omg)[11] = sin(2. * M_PI * (pin_8 + j_pow)); - (*omg)[13] = sin(2. * M_PI * (pin_8 + k_pow)); - (*omg)[15] = sin(2. * M_PI * (pin_8 + j_pow + k_pow)); - *omg += 16; -} - - -EXPORT void reim_fft16_neon(double* dre, double* dim, const void* omega) { - const double* pom = omega; - // Total SIMD register: 28 = 24 + 4 - float64x2x2_t s_re_im; // 2 - float64x2x4_t x_re, x_im, y_re, y_im, t_re, t_im, v_re, v_im; // 32 - - { - /* - Level 2 - ( 8, 24) * ( 0, 1) - ( 9, 25) * ( 0, 1) - ( 10, 26) * ( 0, 1) - ( 11, 27) * ( 0, 1) - ( 12, 28) * ( 0, 1) - ( 13, 29) * ( 0, 1) - ( 14, 30) * ( 0, 1) - ( 15, 31) * ( 0, 1) - - ( 8, 24) = ( 0, 16) - @ - ( 9, 25) = ( 1, 17) - @ - ( 10, 26) = ( 2, 18) - @ - ( 11, 27) = ( 3, 19) - @ - ( 12, 28) = ( 4, 20) - @ - ( 13, 29) = ( 5, 21) - @ - ( 14, 30) = ( 6, 22) - @ - ( 15, 31) = ( 7, 23) - @ - - ( 0, 16) = ( 0, 16) + @ - ( 1, 17) = ( 1, 17) + @ - ( 2, 18) = ( 2, 18) + @ - ( 3, 19) = ( 3, 19) + @ - ( 4, 20) = ( 4, 20) + @ - ( 5, 21) = ( 5, 21) + @ - ( 6, 22) = ( 6, 22) + @ - ( 7, 23) = ( 7, 23) + @ - */ - vload(s_re_im.val[0], pom); - - vloadx4(y_re, dre + 8); - vloadx4(y_im, dim + 8); - - FWD_TOP_LANEx4(v_re, v_im, y_re, y_im, s_re_im.val[0]); - - vloadx4(x_re, dre); - vloadx4(x_im, dim); - - FWD_BOTx4(x_re, x_im, y_re, y_im, v_re, v_im); - - //vstorex4(dre, x_re); - //vstorex4(dim, x_im); - //vstorex4(dre + 8, y_re); - //vstorex4(dim + 8, y_im); - //return; - } - { - /* - Level 3 - - ( 4, 20) * ( 0, 1) - ( 5, 21) * ( 0, 1) - ( 6, 22) * ( 0, 1) - ( 7, 23) * ( 0, 1) - - ( 4, 20) = ( 0, 16) - @ - ( 5, 21) = ( 1, 17) - @ - ( 6, 22) = ( 2, 18) - @ - ( 7, 23) = ( 3, 19) - @ - - ( 0, 16) = ( 0, 16) + @ - ( 1, 17) = ( 1, 17) + @ - ( 2, 18) = ( 2, 18) + @ - ( 3, 19) = ( 3, 19) + @ - - ( 12, 28) * ( 0, 1) - ( 13, 29) * ( 0, 1) - ( 14, 30) * ( 0, 1) - ( 15, 31) * ( 0, 1) - - ( 12, 28) = ( 8, 24) - j@ - ( 13, 29) = ( 9, 25) - j@ - ( 14, 30) = ( 10, 26) - j@ - ( 15, 31) = ( 11, 27) - j@ - - ( 8, 24) = ( 8, 24) + j@ - ( 9, 25) = ( 9, 25) + j@ - ( 10, 26) = ( 10, 26) + j@ - ( 11, 27) = ( 11, 27) + j@ - */ - - //vloadx4(y_re, dre + 8); - //vloadx4(y_im, dim + 8); - //vloadx4(x_re, dre); - //vloadx4(x_im, dim); - - vload(s_re_im.val[0], pom + 2); - - FWD_TOP_LANE(t_re.val[0], t_im.val[0], x_re.val[2], x_im.val[2], s_re_im.val[0]); - FWD_TOP_LANE(t_re.val[1], t_im.val[1], x_re.val[3], x_im.val[3], s_re_im.val[0]); - FWD_TOP_LANE(t_re.val[2], t_im.val[2], y_re.val[2], y_im.val[2], s_re_im.val[0]); - FWD_TOP_LANE(t_re.val[3], t_im.val[3], y_re.val[3], y_im.val[3], s_re_im.val[0]); - - FWD_BOT(x_re.val[0], x_im.val[0], x_re.val[2], x_im.val[2], t_re.val[0], t_im.val[0]); - FWD_BOT(x_re.val[1], x_im.val[1], x_re.val[3], x_im.val[3], t_re.val[1], t_im.val[1]); - FWD_BOTJ(y_re.val[0], y_im.val[0], y_re.val[2], y_im.val[2], t_re.val[2], t_im.val[2]); - FWD_BOTJ(y_re.val[1], y_im.val[1], y_re.val[3], y_im.val[3], t_re.val[3], t_im.val[3]); - - //vstorex4(dre, x_re); - //vstorex4(dim, x_im); - //vstorex4(dre + 8, y_re); - //vstorex4(dim + 8, y_im); - //return; - } - { - /* - Level 4 - - ( 2, 18) * ( 0, 1) - ( 3, 19) * ( 0, 1) - ( 6, 22) * ( 0, 1) - ( 7, 23) * ( 0, 1) - - ( 2, 18) = ( 0, 16) - @ - ( 3, 19) = ( 1, 17) - @ - ( 0, 16) = ( 0, 16) + @ - ( 1, 17) = ( 1, 17) + @ - - ( 6, 22) = ( 4, 20) - j@ - ( 7, 23) = ( 5, 21) - j@ - ( 4, 20) = ( 4, 20) + j@ - ( 5, 21) = ( 5, 21) + j@ - - ( 10, 26) * ( 2, 3) - ( 11, 27) * ( 2, 3) - ( 14, 30) * ( 2, 3) - ( 15, 31) * ( 2, 3) - - ( 10, 26) = ( 8, 24) - @ - ( 11, 27) = ( 9, 25) - @ - ( 8, 24) = ( 8, 24) + @ - ( 9, 25) = ( 9, 25) + @ - - ( 14, 30) = ( 12, 28) - j@ - ( 15, 31) = ( 13, 29) - j@ - ( 12, 28) = ( 12, 28) + j@ - ( 13, 29) = ( 13, 29) + j@ - */ - //vloadx4(y_re, dre + 8); - //vloadx4(y_im, dim + 8); - //vloadx4(x_re, dre); - //vloadx4(x_im, dim); - - vloadx2(s_re_im, pom + 4); - - FWD_TOP_LANE(t_re.val[0], t_im.val[0], x_re.val[1], x_im.val[1], s_re_im.val[0]); - FWD_TOP_LANE(t_re.val[1], t_im.val[1], x_re.val[3], x_im.val[3], s_re_im.val[0]); - FWD_TOP_LANE(t_re.val[2], t_im.val[2], y_re.val[1], y_im.val[1], s_re_im.val[1]); - FWD_TOP_LANE(t_re.val[3], t_im.val[3], y_re.val[3], y_im.val[3], s_re_im.val[1]); - - FWD_BOT(x_re.val[0], x_im.val[0], x_re.val[1], x_im.val[1], t_re.val[0], t_im.val[0]); - FWD_BOTJ(x_re.val[2], x_im.val[2], x_re.val[3], x_im.val[3], t_re.val[1], t_im.val[1]); - FWD_BOT(y_re.val[0], y_im.val[0], y_re.val[1], y_im.val[1], t_re.val[2], t_im.val[2]); - FWD_BOTJ(y_re.val[2], y_im.val[2], y_re.val[3], y_im.val[3], t_re.val[3], t_im.val[3]); - - //vstorex4(dre, x_re); - //vstorex4(dim, x_im); - //vstorex4(dre + 8, y_re); - //vstorex4(dim + 8, y_im); - //return; - } - { - /* - Level 5 - - ( 1, 17) * ( 0, 1) - ( 5, 21) * ( 2, 3) - ------ - ( 1, 17) = ( 0, 16) - @ - ( 5, 21) = ( 4, 20) - @ - ( 0, 16) = ( 0, 16) + @ - ( 4, 20) = ( 4, 20) + @ - - ( 3, 19) * ( 0, 1) - ( 7, 23) * ( 2, 3) - ------ - ( 3, 19) = ( 2, 18) - j@ - ( 7, 23) = ( 6, 22) - j@ - ( 2, 18) = ( 2, 18) + j@ - ( 6, 22) = ( 6, 22) + j@ - - ( 9, 25) * ( 4, 5) - ( 13, 29) * ( 6, 7) - ------ - ( 9, 25) = ( 8, 24) - @ - ( 13, 29) = ( 12, 28) - @ - ( 8, 24) = ( 8, 24) + @ - ( 12, 28) = ( 12, 28) + @ - - ( 11, 27) * ( 4, 5) - ( 15, 31) * ( 6, 7) - ------ - ( 11, 27) = ( 10, 26) - j@ - ( 15, 31) = ( 14, 30) - j@ - ( 10, 26) = ( 10, 26) + j@ - ( 14, 30) = ( 14, 30) + j@ - - before transpose_f64 - x_re: 0, 1 | 2, 3 | 4, 5 | 6, 7 - y_re: 8, 9 | 10, 11 | 12, 13 | 14, 15 - after transpose_f64 - x_re: 0, 4 | 2, 6 | 1, 5 | 3, 7 - y_re: 8, 12| 9, 13| 10, 14 | 11, 15 - after swap - x_re: 0, 4 | 1, 5 | 2, 6 | 3, 7 - y_re: 8, 12| 10, 14 | 9, 13| 11, 15 - */ - - //vloadx4(y_re, dre + 8); - //vloadx4(y_im, dim + 8); - //vloadx4(x_re, dre); - //vloadx4(x_im, dim); - - transpose_f64(x_re, x_re, v_re, 0, 2, 0); - transpose_f64(x_re, x_re, v_re, 1, 3, 1); - transpose_f64(x_im, x_im, v_im, 0, 2, 0); - transpose_f64(x_im, x_im, v_im, 1, 3, 1); - - - v_re.val[0] = x_re.val[2]; - x_re.val[2] = x_re.val[1]; - x_re.val[1] = v_re.val[0]; - - v_im.val[0] = x_im.val[2]; - x_im.val[2] = x_im.val[1]; - x_im.val[1] = v_im.val[0]; - - transpose_f64(y_re, y_re, v_re, 0, 2, 2); - transpose_f64(y_re, y_re, v_re, 1, 3, 3); - transpose_f64(y_im, y_im, v_im, 0, 2, 2); - transpose_f64(y_im, y_im, v_im, 1, 3, 3); - - v_re.val[0] = y_re.val[2]; - y_re.val[2] = y_re.val[1]; - y_re.val[1] = v_re.val[0]; - - v_im.val[0] = y_im.val[2]; - y_im.val[2] = y_im.val[1]; - y_im.val[1] = v_im.val[0]; - - //double pom8[] = {pom[8], pom[12], pom[9], pom[13]}; - vload2(s_re_im, pom+8); - //vload2(s_re_im, pom8); - - FWD_TOP(t_re.val[0], t_im.val[0], x_re.val[1], x_im.val[1], s_re_im.val[0], s_re_im.val[1]); - FWD_TOP(t_re.val[1], t_im.val[1], x_re.val[3], x_im.val[3], s_re_im.val[0], s_re_im.val[1]); - - //double pom12[] = {pom[10], pom[14], pom[11], pom[15]}; - vload2(s_re_im, pom+12); - //vload2(s_re_im, pom12); - - FWD_TOP(t_re.val[2], t_im.val[2], y_re.val[1], y_im.val[1], s_re_im.val[0], s_re_im.val[1]); - FWD_TOP(t_re.val[3], t_im.val[3], y_re.val[3], y_im.val[3], s_re_im.val[0], s_re_im.val[1]); - - FWD_BOT (x_re.val[0], x_im.val[0], x_re.val[1], x_im.val[1], t_re.val[0], t_im.val[0]); - FWD_BOTJ(x_re.val[2], x_im.val[2], x_re.val[3], x_im.val[3], t_re.val[1], t_im.val[1]); - - vstore4(dre, x_re); - vstore4(dim, x_im); - - FWD_BOT (y_re.val[0], y_im.val[0], y_re.val[1], y_im.val[1], t_re.val[2], t_im.val[2]); - FWD_BOTJ(y_re.val[2], y_im.val[2], y_re.val[3], y_im.val[3], t_re.val[3], t_im.val[3]); - - vstore4(dre+8, y_re); - vstore4(dim+8, y_im); - } -} - -void reim_twiddle_fft_neon(uint64_t h, double* re, double* im, double om[2]) { - // Total SIMD register: 28 = 24 + 4 - if (h<8) abort(); // bug - float64x2_t s_re_im; // 2 - float64x2x4_t x_re, x_im, y_re, y_im, v_re, v_im; // 32 - vload(s_re_im, om); - for (uint64_t blk = 0; blk < h; blk+=8) { - double* dre = re + blk; - double* dim = im + blk; - vloadx4(y_re, dre + h); - vloadx4(y_im, dim + h); - FWD_TOP_LANEx4(v_re, v_im, y_re, y_im, s_re_im); - vloadx4(x_re, dre); - vloadx4(x_im, dim); - FWD_BOTx4(x_re, x_im, y_re, y_im, v_re, v_im); - vstorex4(dre, x_re); - vstorex4(dim, x_im); - vstorex4(dre + h, y_re); - vstorex4(dim + h, y_im); - } -} - -void reim_ctwiddle(double* ra, double* ia, double* rb, double* ib, double omre, double omim); -// i (omre + i omim) = -omim + i omre -void reim_citwiddle(double* ra, double* ia, double* rb, double* ib, double omre, double omim); - -void reim_bitwiddle_fft_neon(uint64_t h, double* re, double* im, double om[4]) { - // Total SIMD register: 28 = 24 + 4 - if (h<4) abort(); // bug - double* r0 = re; - double* r1 = re + h; - double* r2 = re + 2*h; - double* r3 = re + 3*h; - double* i0 = im; - double* i1 = im + h; - double* i2 = im + 2*h; - double* i3 = im + 3*h; - float64x2x2_t s_re_im; // 2 - float64x2x4_t v_re, v_im; // 2 - float64x2x2_t x0_re, x0_im, x1_re, x1_im; // 32 - float64x2x2_t x2_re, x2_im, x3_re, x3_im; // 32 - vloadx2(s_re_im, om); - for (uint64_t blk=0; blk<h; blk+=4) { - { - vloadx2(x2_re, r2 + blk); - vloadx2(x3_re, r3 + blk); - vloadx2(x2_im, i2 + blk); - vloadx2(x3_im, i3 + blk); - FWD_TOP_LANE(v_re.val[0], v_im.val[0], x2_re.val[0], x2_im.val[0], s_re_im.val[0]); - FWD_TOP_LANE(v_re.val[1], v_im.val[1], x2_re.val[1], x2_im.val[1], s_re_im.val[0]); - FWD_TOP_LANE(v_re.val[2], v_im.val[2], x3_re.val[0], x3_im.val[0], s_re_im.val[0]); - FWD_TOP_LANE(v_re.val[3], v_im.val[3], x3_re.val[1], x3_im.val[1], s_re_im.val[0]); - vloadx2(x0_re, r0 + blk); - vloadx2(x1_re, r1 + blk); - vloadx2(x0_im, i0 + blk); - vloadx2(x1_im, i1 + blk); - FWD_BOT(x0_re.val[0], x0_im.val[0], x2_re.val[0], x2_im.val[0], v_re.val[0], v_im.val[0]); - FWD_BOT(x0_re.val[1], x0_im.val[1], x2_re.val[1], x2_im.val[1], v_re.val[1], v_im.val[1]); - FWD_BOT(x1_re.val[0], x1_im.val[0], x3_re.val[0], x3_im.val[0], v_re.val[2], v_im.val[2]); - FWD_BOT(x1_re.val[1], x1_im.val[1], x3_re.val[1], x3_im.val[1], v_re.val[3], v_im.val[3]); - } - { - FWD_TOP_LANE(v_re.val[0], v_im.val[0], x1_re.val[0], x1_im.val[0], s_re_im.val[1]); - FWD_TOP_LANE(v_re.val[1], v_im.val[1], x1_re.val[1], x1_im.val[1], s_re_im.val[1]); - FWD_TOP_LANE(v_re.val[2], v_im.val[2], x3_re.val[0], x3_im.val[0], s_re_im.val[1]); - FWD_TOP_LANE(v_re.val[3], v_im.val[3], x3_re.val[1], x3_im.val[1], s_re_im.val[1]); - FWD_BOT(x0_re.val[0], x0_im.val[0], x1_re.val[0], x1_im.val[0], v_re.val[0], v_im.val[0]); - FWD_BOT(x0_re.val[1], x0_im.val[1], x1_re.val[1], x1_im.val[1], v_re.val[1], v_im.val[1]); - FWD_BOTJ(x2_re.val[0], x2_im.val[0], x3_re.val[0], x3_im.val[0], v_re.val[2], v_im.val[2]); - FWD_BOTJ(x2_re.val[1], x2_im.val[1], x3_re.val[1], x3_im.val[1], v_re.val[3], v_im.val[3]); - vstorex2(r0 + blk, x0_re); - vstorex2(r1 + blk, x1_re); - vstorex2(r2 + blk, x2_re); - vstorex2(r3 + blk, x3_re); - vstorex2(i0 + blk, x0_im); - vstorex2(i1 + blk, x1_im); - vstorex2(i2 + blk, x2_im); - vstorex2(i3 + blk, x3_im); - } - } -} - - -#if 0 -static void ZfN(iFFT_log2)(fpr *f) -{ - /* - y_re: 1 = (2 - 3) * 5 + (0 - 1) * 4 - y_im: 3 = (2 - 3) * 4 - (0 - 1) * 5 - x_re: 0 = 0 + 1 - x_im: 2 = 2 + 3 - - Turn out this vectorize code is too short to be executed, - the scalar version is consistently faster - - float64x2x2_t tmp; - float64x2_t v, s, t; - - // 0: 0, 2 - // 1: 1, 3 - - vload2(tmp, &f[0]); - vload(s, &fpr_gm_tab[4]); - - vfsub(v, tmp.val[0], tmp.val[1]); - vfadd(tmp.val[0], tmp.val[0], tmp.val[1]); - - // y_im: 3 = (2 - 3) * 4 - (0 - 1) * 5 - // y_re: 1 = (2 - 3) * 5 + (0 - 1) * 4 - vswap(t, v); - - vfmul_lane(tmp.val[1], s, t, 0); - vfcmla_90(tmp.val[1], t, s); - - vfmuln(tmp.val[0], tmp.val[0], 0.5); - vfmuln(tmp.val[1], tmp.val[1], 0.5); - - vswap(tmp.val[1], tmp.val[1]); - - vstore2(&f[0], tmp); - */ - - fpr x_re, x_im, y_re, y_im, s; - x_re = f[0]; - y_re = f[1]; - x_im = f[2]; - y_im = f[3]; - s = fpr_tab_log2[0] * 0.5; - - f[0] = (x_re + y_re) * 0.5; - f[2] = (x_im + y_im) * 0.5; - - x_re = (x_re - y_re) * s; - x_im = (x_im - y_im) * s; - - f[1] = x_im + x_re; - f[3] = x_im - x_re; -} - -static void ZfN(iFFT_log3)(fpr *f) -{ - /* - * Total instructions: 27 - y_re: 1 = (4 - 5) * 9 + (0 - 1) * 8 - y_re: 3 = (6 - 7) * 11 + (2 - 3) * 10 - y_im: 5 = (4 - 5) * 8 - (0 - 1) * 9 - y_im: 7 = (6 - 7) * 10 - (2 - 3) * 11 - x_re: 0 = 0 + 1 - x_re: 2 = 2 + 3 - x_im: 4 = 4 + 5 - x_im: 6 = 6 + 7 - */ - // 0: 0, 2 - 0: 0, 4 - // 1: 1, 3 - 1: 1, 5 - // 2: 4, 6 - 2: 2, 6 - // 3: 5, 7 - 3: 3, 7 - float64x2x4_t tmp; - float64x2x2_t x_re_im, y_re_im, v, s_re_im; - - vload2(x_re_im, &f[0]); - vload2(y_re_im, &f[4]); - - vfsub(v.val[0], x_re_im.val[0], x_re_im.val[1]); - vfsub(v.val[1], y_re_im.val[0], y_re_im.val[1]); - vfadd(x_re_im.val[0], x_re_im.val[0], x_re_im.val[1]); - vfadd(x_re_im.val[1], y_re_im.val[0], y_re_im.val[1]); - - // 0: 8, 10 - // 1: 9, 11 - vload2(s_re_im, &fpr_tab_log3[0]); - - vfmul(y_re_im.val[0], v.val[1], s_re_im.val[1]); - vfmla(y_re_im.val[0], y_re_im.val[0], v.val[0], s_re_im.val[0]); - vfmul(y_re_im.val[1], v.val[1], s_re_im.val[0]); - vfmls(y_re_im.val[1], y_re_im.val[1], v.val[0], s_re_im.val[1]); - - // x: 0,2 | 4,6 - // y: 1,3 | 5,7 - tmp.val[0] = vtrn1q_f64(x_re_im.val[0], y_re_im.val[0]); - tmp.val[1] = vtrn2q_f64(x_re_im.val[0], y_re_im.val[0]); - tmp.val[2] = vtrn1q_f64(x_re_im.val[1], y_re_im.val[1]); - tmp.val[3] = vtrn2q_f64(x_re_im.val[1], y_re_im.val[1]); - // tmp: 0,1 | 2,3 | 4,5 | 6,7 - /* - y_re: 2 = (4 - 6) * 4 + (0 - 2) * 4 - y_re: 3 = (5 - 7) * 4 + (1 - 3) * 4 - y_im: 6 = (4 - 6) * 4 - (0 - 2) * 4 - y_im: 7 = (5 - 7) * 4 - (1 - 3) * 4 - x_re: 0 = 0 + 2 - x_re: 1 = 1 + 3 - x_im: 4 = 4 + 6 - x_im: 5 = 5 + 7 - */ - s_re_im.val[0] = vld1q_dup_f64(&fpr_tab_log2[0]); - - vfadd(x_re_im.val[0], tmp.val[0], tmp.val[1]); - vfadd(x_re_im.val[1], tmp.val[2], tmp.val[3]); - vfsub(v.val[0], tmp.val[0], tmp.val[1]); - vfsub(v.val[1], tmp.val[2], tmp.val[3]); - - vfmuln(tmp.val[0], x_re_im.val[0], 0.25); - vfmuln(tmp.val[2], x_re_im.val[1], 0.25); - - vfmuln(s_re_im.val[0], s_re_im.val[0], 0.25); - - vfmul(y_re_im.val[0], v.val[0], s_re_im.val[0]); - vfmul(y_re_im.val[1], v.val[1], s_re_im.val[0]); - - vfadd(tmp.val[1], y_re_im.val[1], y_re_im.val[0]); - vfsub(tmp.val[3], y_re_im.val[1], y_re_im.val[0]); - - vstorex4(&f[0], tmp); -} - -static void ZfN(iFFT_log4)(fpr *f) -{ - /* - * ( 0, 8) - ( 1, 9) - * ( 4, 12) - ( 5, 13) - * - * ( 0, 8) + ( 1, 9) - * ( 4, 12) + ( 5, 13) - * - * ( 3, 11) - ( 2, 10) - * ( 7, 15) - ( 6, 14) - * - * ( 2, 10) + ( 3, 11) - * ( 6, 14) + ( 7, 15) - * - * ( 1, 9) = @ * ( 0, 1) - * ( 5, 13) = @ * ( 2, 3) - * - * ( 3, 11) = j@ * ( 0, 1) - * ( 7, 15) = j@ * ( 2, 3) - */ - - float64x2x4_t re, im, t; - float64x2x2_t t_re, t_im, s_re_im; - - vload4(re, &f[0]); - vload4(im, &f[8]); - - INV_TOPJ (t_re.val[0], t_im.val[0], re.val[0], im.val[0], re.val[1], im.val[1]); - INV_TOPJm(t_re.val[1], t_im.val[1], re.val[2], im.val[2], re.val[3], im.val[3]); - - vload2(s_re_im, &fpr_tab_log4[0]); - - INV_BOTJ (re.val[1], im.val[1], t_re.val[0], t_im.val[0], s_re_im.val[0], s_re_im.val[1]); - INV_BOTJm(re.val[3], im.val[3], t_re.val[1], t_im.val[1], s_re_im.val[0], s_re_im.val[1]); - - /* - * ( 0, 8) - ( 2, 10) - * ( 1, 9) - ( 3, 11) - * - * ( 0, 8) + ( 2, 10) - * ( 1, 9) + ( 3, 11) - * - * ( 2, 10) = @ * ( 0, 1) - * ( 3, 11) = @ * ( 0, 1) - * - * ( 6, 14) - ( 4, 12) - * ( 7, 15) - ( 5, 13) - * - * ( 4, 12) + ( 6, 14) - * ( 5, 13) + ( 7, 15) - * - * ( 6, 14) = j@ * ( 0, 1) - * ( 7, 15) = j@ * ( 0, 1) - */ - - // re: 0, 4 | 1, 5 | 2, 6 | 3, 7 - // im: 8, 12| 9, 13|10, 14|11, 15 - - transpose_f64(re, re, t, 0, 1, 0); - transpose_f64(re, re, t, 2, 3, 1); - transpose_f64(im, im, t, 0, 1, 2); - transpose_f64(im, im, t, 2, 3, 3); - - // re: 0, 1 | 4, 5 | 2, 3 | 6, 7 - // im: 8, 9 | 12, 13|10, 11| 14, 15 - t.val[0] = re.val[1]; - re.val[1] = re.val[2]; - re.val[2] = t.val[0]; - - t.val[1] = im.val[1]; - im.val[1] = im.val[2]; - im.val[2] = t.val[1]; - - // re: 0, 1 | 2, 3| 4, 5 | 6, 7 - // im: 8, 9 | 10, 11| 12, 13| 14, 15 - - INV_TOPJ (t_re.val[0], t_im.val[0], re.val[0], im.val[0], re.val[1], im.val[1]); - INV_TOPJm(t_re.val[1], t_im.val[1], re.val[2], im.val[2], re.val[3], im.val[3]); - - vload(s_re_im.val[0], &fpr_tab_log3[0]); - - INV_BOTJ_LANE (re.val[1], im.val[1], t_re.val[0], t_im.val[0], s_re_im.val[0]); - INV_BOTJm_LANE(re.val[3], im.val[3], t_re.val[1], t_im.val[1], s_re_im.val[0]); - - /* - * ( 0, 8) - ( 4, 12) - * ( 1, 9) - ( 5, 13) - * ( 0, 8) + ( 4, 12) - * ( 1, 9) + ( 5, 13) - * - * ( 2, 10) - ( 6, 14) - * ( 3, 11) - ( 7, 15) - * ( 2, 10) + ( 6, 14) - * ( 3, 11) + ( 7, 15) - * - * ( 4, 12) = @ * ( 0, 1) - * ( 5, 13) = @ * ( 0, 1) - * - * ( 6, 14) = @ * ( 0, 1) - * ( 7, 15) = @ * ( 0, 1) - */ - - INV_TOPJ(t_re.val[0], t_im.val[0], re.val[0], im.val[0], re.val[2], im.val[2]); - INV_TOPJ(t_re.val[1], t_im.val[1], re.val[1], im.val[1], re.val[3], im.val[3]); - - vfmuln(re.val[0], re.val[0], 0.12500000000); - vfmuln(re.val[1], re.val[1], 0.12500000000); - vfmuln(im.val[0], im.val[0], 0.12500000000); - vfmuln(im.val[1], im.val[1], 0.12500000000); - - s_re_im.val[0] = vld1q_dup_f64(&fpr_tab_log2[0]); - - vfmuln(s_re_im.val[0], s_re_im.val[0], 0.12500000000); - - vfmul(t_re.val[0], t_re.val[0], s_re_im.val[0]); - vfmul(t_re.val[1], t_re.val[1], s_re_im.val[0]); - vfmul(t_im.val[0], t_im.val[0], s_re_im.val[0]); - vfmul(t_im.val[1], t_im.val[1], s_re_im.val[0]); - - vfsub(im.val[2], t_im.val[0], t_re.val[0]); - vfsub(im.val[3], t_im.val[1], t_re.val[1]); - vfadd(re.val[2], t_im.val[0], t_re.val[0]); - vfadd(re.val[3], t_im.val[1], t_re.val[1]); - - vstorex4(&f[0], re); - vstorex4(&f[8], im); -} - -static - void ZfN(iFFT_log5)(fpr *f, const unsigned logn, const unsigned last) -{ - // Total SIMD register: 26 = 24 + 2 - float64x2x4_t x_re, x_im, y_re, y_im, t_re, t_im; // 24 - float64x2x2_t s_re_im; // 2 - const unsigned n = 1 << logn; - const unsigned hn = n >> 1; - - int level = logn; - const fpr *fpr_tab5 = fpr_table[level--], - *fpr_tab4 = fpr_table[level--], - *fpr_tab3 = fpr_table[level--], - *fpr_tab2 = fpr_table[level]; - int k2 = 0, k3 = 0, k4 = 0, k5 = 0; - - for (unsigned j = 0; j < hn; j += 16) - { - /* - * ( 0, 16) - ( 1, 17) - * ( 4, 20) - ( 5, 21) - * ( 0, 16) + ( 1, 17) - * ( 4, 20) + ( 5, 21) - * ( 1, 17) = @ * ( 0, 1) - * ( 5, 21) = @ * ( 2, 3) - * - * ( 2, 18) - ( 3, 19) - * ( 6, 22) - ( 7, 23) - * ( 2, 18) + ( 3, 19) - * ( 6, 22) + ( 7, 23) - * ( 3, 19) = j@ * ( 0, 1) - * ( 7, 23) = j@ * ( 2, 3) - * - * ( 8, 24) - ( 9, 25) - * ( 12, 28) - ( 13, 29) - * ( 8, 24) + ( 9, 25) - * ( 12, 28) + ( 13, 29) - * ( 9, 25) = @ * ( 4, 5) - * ( 13, 29) = @ * ( 6, 7) - * - * ( 10, 26) - ( 11, 27) - * ( 14, 30) - ( 15, 31) - * ( 10, 26) + ( 11, 27) - * ( 14, 30) + ( 15, 31) - * ( 11, 27) = j@ * ( 4, 5) - * ( 15, 31) = j@ * ( 6, 7) - */ - - vload4(x_re, &f[j]); - vload4(x_im, &f[j + hn]); - - INV_TOPJ(t_re.val[0], t_im.val[0], x_re.val[0], x_im.val[0], x_re.val[1], x_im.val[1]); - INV_TOPJm(t_re.val[2], t_im.val[2], x_re.val[2], x_im.val[2], x_re.val[3], x_im.val[3]); - - vload4(y_re, &f[j + 8]); - vload4(y_im, &f[j + 8 + hn]) - - INV_TOPJ(t_re.val[1], t_im.val[1], y_re.val[0], y_im.val[0], y_re.val[1], y_im.val[1]); - INV_TOPJm(t_re.val[3], t_im.val[3], y_re.val[2], y_im.val[2], y_re.val[3], y_im.val[3]); - - vload2(s_re_im, &fpr_tab5[k5]); - k5 += 4; - - INV_BOTJ (x_re.val[1], x_im.val[1], t_re.val[0], t_im.val[0], s_re_im.val[0], s_re_im.val[1]); - INV_BOTJm(x_re.val[3], x_im.val[3], t_re.val[2], t_im.val[2], s_re_im.val[0], s_re_im.val[1]); - - vload2(s_re_im, &fpr_tab5[k5]); - k5 += 4; - - INV_BOTJ (y_re.val[1], y_im.val[1], t_re.val[1], t_im.val[1], s_re_im.val[0], s_re_im.val[1]); - INV_BOTJm(y_re.val[3], y_im.val[3], t_re.val[3], t_im.val[3], s_re_im.val[0], s_re_im.val[1]); - - - // x_re: 0, 4 | 1, 5 | 2, 6 | 3, 7 - // y_re: 8, 12| 9, 13|10, 14|11, 15 - - transpose_f64(x_re, x_re, t_re, 0, 1, 0); - transpose_f64(x_re, x_re, t_re, 2, 3, 1); - transpose_f64(y_re, y_re, t_re, 0, 1, 2); - transpose_f64(y_re, y_re, t_re, 2, 3, 3); - - transpose_f64(x_im, x_im, t_im, 0, 1, 0); - transpose_f64(x_im, x_im, t_im, 2, 3, 1); - transpose_f64(y_im, y_im, t_im, 0, 1, 2); - transpose_f64(y_im, y_im, t_im, 2, 3, 3); - - // x_re: 0, 1 | 4, 5 | 2, 3 | 6, 7 - // y_re: 8, 9 | 12,13|10,11 |14, 15 - - t_re.val[0] = x_re.val[1]; - x_re.val[1] = x_re.val[2]; - x_re.val[2] = t_re.val[0]; - - t_re.val[1] = y_re.val[1]; - y_re.val[1] = y_re.val[2]; - y_re.val[2] = t_re.val[1]; - - - t_im.val[0] = x_im.val[1]; - x_im.val[1] = x_im.val[2]; - x_im.val[2] = t_im.val[0]; - - t_im.val[1] = y_im.val[1]; - y_im.val[1] = y_im.val[2]; - y_im.val[2] = t_im.val[1]; - // x_re: 0, 1 | 2, 3| 4, 5 | 6, 7 - // y_re: 8, 9 | 10, 11| 12, 13| 14, 15 - - /* - * ( 0, 16) - ( 2, 18) - * ( 1, 17) - ( 3, 19) - * ( 0, 16) + ( 2, 18) - * ( 1, 17) + ( 3, 19) - * ( 2, 18) = @ * ( 0, 1) - * ( 3, 19) = @ * ( 0, 1) - * - * ( 4, 20) - ( 6, 22) - * ( 5, 21) - ( 7, 23) - * ( 4, 20) + ( 6, 22) - * ( 5, 21) + ( 7, 23) - * ( 6, 22) = j@ * ( 0, 1) - * ( 7, 23) = j@ * ( 0, 1) - * - * ( 8, 24) - ( 10, 26) - * ( 9, 25) - ( 11, 27) - * ( 8, 24) + ( 10, 26) - * ( 9, 25) + ( 11, 27) - * ( 10, 26) = @ * ( 2, 3) - * ( 11, 27) = @ * ( 2, 3) - * - * ( 12, 28) - ( 14, 30) - * ( 13, 29) - ( 15, 31) - * ( 12, 28) + ( 14, 30) - * ( 13, 29) + ( 15, 31) - * ( 14, 30) = j@ * ( 2, 3) - * ( 15, 31) = j@ * ( 2, 3) - */ - - INV_TOPJ (t_re.val[0], t_im.val[0], x_re.val[0], x_im.val[0], x_re.val[1], x_im.val[1]); - INV_TOPJm(t_re.val[1], t_im.val[1], x_re.val[2], x_im.val[2], x_re.val[3], x_im.val[3]); - - INV_TOPJ (t_re.val[2], t_im.val[2], y_re.val[0], y_im.val[0], y_re.val[1], y_im.val[1]); - INV_TOPJm(t_re.val[3], t_im.val[3], y_re.val[2], y_im.val[2], y_re.val[3], y_im.val[3]); - - vloadx2(s_re_im, &fpr_tab4[k4]); - k4 += 4; - - INV_BOTJ_LANE (x_re.val[1], x_im.val[1], t_re.val[0], t_im.val[0], s_re_im.val[0]); - INV_BOTJm_LANE(x_re.val[3], x_im.val[3], t_re.val[1], t_im.val[1], s_re_im.val[0]); - - INV_BOTJ_LANE (y_re.val[1], y_im.val[1], t_re.val[2], t_im.val[2], s_re_im.val[1]); - INV_BOTJm_LANE(y_re.val[3], y_im.val[3], t_re.val[3], t_im.val[3], s_re_im.val[1]); - - /* - * ( 0, 16) - ( 4, 20) - * ( 1, 17) - ( 5, 21) - * ( 0, 16) + ( 4, 20) - * ( 1, 17) + ( 5, 21) - * ( 4, 20) = @ * ( 0, 1) - * ( 5, 21) = @ * ( 0, 1) - * - * ( 2, 18) - ( 6, 22) - * ( 3, 19) - ( 7, 23) - * ( 2, 18) + ( 6, 22) - * ( 3, 19) + ( 7, 23) - * ( 6, 22) = @ * ( 0, 1) - * ( 7, 23) = @ * ( 0, 1) - * - * ( 8, 24) - ( 12, 28) - * ( 9, 25) - ( 13, 29) - * ( 8, 24) + ( 12, 28) - * ( 9, 25) + ( 13, 29) - * ( 12, 28) = j@ * ( 0, 1) - * ( 13, 29) = j@ * ( 0, 1) - * - * ( 10, 26) - ( 14, 30) - * ( 11, 27) - ( 15, 31) - * ( 10, 26) + ( 14, 30) - * ( 11, 27) + ( 15, 31) - * ( 14, 30) = j@ * ( 0, 1) - * ( 15, 31) = j@ * ( 0, 1) - */ - - INV_TOPJ (t_re.val[0], t_im.val[0], x_re.val[0], x_im.val[0], x_re.val[2], x_im.val[2]); - INV_TOPJ (t_re.val[1], t_im.val[1], x_re.val[1], x_im.val[1], x_re.val[3], x_im.val[3]); - - INV_TOPJm(t_re.val[2], t_im.val[2], y_re.val[0], y_im.val[0], y_re.val[2], y_im.val[2]); - INV_TOPJm(t_re.val[3], t_im.val[3], y_re.val[1], y_im.val[1], y_re.val[3], y_im.val[3]); - - vload(s_re_im.val[0], &fpr_tab3[k3]); - k3 += 2; - - INV_BOTJ_LANE(x_re.val[2], x_im.val[2], t_re.val[0], t_im.val[0], s_re_im.val[0]); - INV_BOTJ_LANE(x_re.val[3], x_im.val[3], t_re.val[1], t_im.val[1], s_re_im.val[0]); - - INV_BOTJm_LANE(y_re.val[2], y_im.val[2], t_re.val[2], t_im.val[2], s_re_im.val[0]); - INV_BOTJm_LANE(y_re.val[3], y_im.val[3], t_re.val[3], t_im.val[3], s_re_im.val[0]); - - /* - * ( 0, 16) - ( 8, 24) - * ( 1, 17) - ( 9, 25) - * ( 0, 16) + ( 8, 24) - * ( 1, 17) + ( 9, 25) - * ( 8, 24) = @ * ( 0, 1) - * ( 9, 25) = @ * ( 0, 1) - * - * ( 2, 18) - ( 10, 26) - * ( 3, 19) - ( 11, 27) - * ( 2, 18) + ( 10, 26) - * ( 3, 19) + ( 11, 27) - * ( 10, 26) = @ * ( 0, 1) - * ( 11, 27) = @ * ( 0, 1) - * - * ( 4, 20) - ( 12, 28) - * ( 5, 21) - ( 13, 29) - * ( 4, 20) + ( 12, 28) - * ( 5, 21) + ( 13, 29) - * ( 12, 28) = @ * ( 0, 1) - * ( 13, 29) = @ * ( 0, 1) - * - * ( 6, 22) - ( 14, 30) - * ( 7, 23) - ( 15, 31) - * ( 6, 22) + ( 14, 30) - * ( 7, 23) + ( 15, 31) - * ( 14, 30) = @ * ( 0, 1) - * ( 15, 31) = @ * ( 0, 1) - */ - - - if ( (j >> 4) & 1) - { - INV_TOPJmx4(t_re, t_im, x_re, x_im, y_re, y_im); - } - else - { - INV_TOPJx4(t_re, t_im, x_re, x_im, y_re, y_im); - } - - vload(s_re_im.val[0], &fpr_tab2[k2]); - k2 += 2 * ((j & 31) == 16); - - if (last) - { - vfmuln(s_re_im.val[0], s_re_im.val[0], fpr_p2_tab[logn]); - vfmulnx4(x_re, x_re, fpr_p2_tab[logn]); - vfmulnx4(x_im, x_im, fpr_p2_tab[logn]); - } - vstorex4(&f[j], x_re); - vstorex4(&f[j + hn], x_im); - - if (logn == 5) - { - // Special case in fpr_tab_log2 where re == im - vfmulx4_i(t_re, t_re, s_re_im.val[0]); - vfmulx4_i(t_im, t_im, s_re_im.val[0]); - - vfaddx4(y_re, t_im, t_re); - vfsubx4(y_im, t_im, t_re); - } - else - { - if ((j >> 4) & 1) - { - INV_BOTJm_LANEx4(y_re, y_im, t_re, t_im, s_re_im.val[0]); - } - else - { - INV_BOTJ_LANEx4(y_re, y_im, t_re, t_im, s_re_im.val[0]); - } - } - - vstorex4(&f[j + 8], y_re); - vstorex4(&f[j + 8 + hn], y_im); - } -} - -static - void ZfN(iFFT_logn1)(fpr *f, const unsigned logn, const unsigned last) -{ - // Total SIMD register 26 = 24 + 2 - float64x2x4_t a_re, a_im, b_re, b_im, t_re, t_im; // 24 - float64x2_t s_re_im; // 2 - - const unsigned n = 1 << logn; - const unsigned hn = n >> 1; - const unsigned ht = n >> 2; - - for (unsigned j = 0; j < ht; j+= 8) - { - vloadx4(a_re, &f[j]); - vloadx4(a_im, &f[j + hn]); - vloadx4(b_re, &f[j + ht]); - vloadx4(b_im, &f[j + ht + hn]); - - INV_TOPJx4(t_re, t_im, a_re, a_im, b_re, b_im); - - s_re_im = vld1q_dup_f64(&fpr_tab_log2[0]); - - if (last) - { - vfmuln(s_re_im, s_re_im, fpr_p2_tab[logn]); - vfmulnx4(a_re, a_re, fpr_p2_tab[logn]); - vfmulnx4(a_im, a_im, fpr_p2_tab[logn]); - } - - vstorex4(&f[j], a_re); - vstorex4(&f[j + hn], a_im); - - vfmulx4_i(t_re, t_re, s_re_im); - vfmulx4_i(t_im, t_im, s_re_im); - - vfaddx4(b_re, t_im, t_re); - vfsubx4(b_im, t_im, t_re); - - vstorex4(&f[j + ht], b_re); - vstorex4(&f[j + ht + hn], b_im); - } -} - -// static -// void ZfN(iFFT_logn2)(fpr *f, const unsigned logn, const unsigned level, unsigned last) -// { -// const unsigned int falcon_n = 1 << logn; -// const unsigned int hn = falcon_n >> 1; - -// // Total SIMD register: 26 = 16 + 8 + 2 -// float64x2x4_t t_re, t_im; // 8 -// float64x2x2_t x1_re, x2_re, x1_im, x2_im, -// y1_re, y2_re, y1_im, y2_im; // 16 -// float64x2_t s1_re_im, s2_re_im; // 2 - -// const fpr *fpr_inv_tab1 = NULL, *fpr_inv_tab2 = NULL; -// unsigned l, len, start, j, k1, k2; -// unsigned bar = logn - 4 - 2; -// unsigned Jm; - -// for (l = 4; l < logn - level - 1; l += 2) -// { -// len = 1 << l; -// last -= 1; -// fpr_inv_tab1 = fpr_table[bar--]; -// fpr_inv_tab2 = fpr_table[bar--]; -// k1 = 0; k2 = 0; - -// for (start = 0; start < hn; start += 1 << (l + 2)) -// { -// vload(s1_re_im, &fpr_inv_tab1[k1]); -// vload(s2_re_im, &fpr_inv_tab2[k2]); -// k1 += 2; -// k2 += 2 * ((start & 127) == 64); -// if (!last) -// { -// vfmuln(s2_re_im, s2_re_im, fpr_p2_tab[logn]); -// } -// Jm = (start >> (l+ 2)) & 1; -// for (j = start; j < start + len; j += 4) -// { -// /* -// Level 6 -// * ( 0, 64) - ( 16, 80) -// * ( 1, 65) - ( 17, 81) -// * ( 0, 64) + ( 16, 80) -// * ( 1, 65) + ( 17, 81) -// * ( 16, 80) = @ * ( 0, 1) -// * ( 17, 81) = @ * ( 0, 1) -// * -// * ( 2, 66) - ( 18, 82) -// * ( 3, 67) - ( 19, 83) -// * ( 2, 66) + ( 18, 82) -// * ( 3, 67) + ( 19, 83) -// * ( 18, 82) = @ * ( 0, 1) -// * ( 19, 83) = @ * ( 0, 1) -// * -// * ( 32, 96) - ( 48, 112) -// * ( 33, 97) - ( 49, 113) -// * ( 32, 96) + ( 48, 112) -// * ( 33, 97) + ( 49, 113) -// * ( 48, 112) = j@ * ( 0, 1) -// * ( 49, 113) = j@ * ( 0, 1) -// * -// * ( 34, 98) - ( 50, 114) -// * ( 35, 99) - ( 51, 115) -// * ( 34, 98) + ( 50, 114) -// * ( 35, 99) + ( 51, 115) -// * ( 50, 114) = j@ * ( 0, 1) -// * ( 51, 115) = j@ * ( 0, 1) -// */ -// // x1: 0 -> 4 | 64 -> 67 -// // y1: 16 -> 19 | 80 -> 81 -// // x2: 32 -> 35 | 96 -> 99 -// // y2: 48 -> 51 | 112 -> 115 -// vloadx2(x1_re, &f[j]); -// vloadx2(x1_im, &f[j + hn]); -// vloadx2(y1_re, &f[j + len]); -// vloadx2(y1_im, &f[j + len + hn]); - -// INV_TOPJ (t_re.val[0], t_im.val[0], x1_re.val[0], x1_im.val[0], y1_re.val[0], y1_im.val[0]); -// INV_TOPJ (t_re.val[1], t_im.val[1], x1_re.val[1], x1_im.val[1], y1_re.val[1], y1_im.val[1]); - -// vloadx2(x2_re, &f[j + 2*len]); -// vloadx2(x2_im, &f[j + 2*len + hn]); -// vloadx2(y2_re, &f[j + 3*len]); -// vloadx2(y2_im, &f[j + 3*len + hn]); - -// INV_TOPJm(t_re.val[2], t_im.val[2], x2_re.val[0], x2_im.val[0], y2_re.val[0], y2_im.val[0]); -// INV_TOPJm(t_re.val[3], t_im.val[3], x2_re.val[1], x2_im.val[1], y2_re.val[1], y2_im.val[1]); - -// INV_BOTJ_LANE (y1_re.val[0], y1_im.val[0], t_re.val[0], t_im.val[0], s1_re_im); -// INV_BOTJ_LANE (y1_re.val[1], y1_im.val[1], t_re.val[1], t_im.val[1], s1_re_im); - -// INV_BOTJm_LANE(y2_re.val[0], y2_im.val[0], t_re.val[2], t_im.val[2], s1_re_im); -// INV_BOTJm_LANE(y2_re.val[1], y2_im.val[1], t_re.val[3], t_im.val[3], s1_re_im); -// /* -// * Level 7 -// * ( 0, 64) - ( 32, 96) -// * ( 1, 65) - ( 33, 97) -// * ( 0, 64) + ( 32, 96) -// * ( 1, 65) + ( 33, 97) -// * ( 32, 96) = @ * ( 0, 1) -// * ( 33, 97) = @ * ( 0, 1) -// * -// * ( 2, 66) - ( 34, 98) -// * ( 3, 67) - ( 35, 99) -// * ( 2, 66) + ( 34, 98) -// * ( 3, 67) + ( 35, 99) -// * ( 34, 98) = @ * ( 0, 1) -// * ( 35, 99) = @ * ( 0, 1) -// * ---- -// * ( 16, 80) - ( 48, 112) -// * ( 17, 81) - ( 49, 113) -// * ( 16, 80) + ( 48, 112) -// * ( 17, 81) + ( 49, 113) -// * ( 48, 112) = @ * ( 0, 1) -// * ( 49, 113) = @ * ( 0, 1) -// * -// * ( 18, 82) - ( 50, 114) -// * ( 19, 83) - ( 51, 115) -// * ( 18, 82) + ( 50, 114) -// * ( 19, 83) + ( 51, 115) -// * ( 50, 114) = @ * ( 0, 1) -// * ( 51, 115) = @ * ( 0, 1) -// */ - -// if (Jm) -// { -// INV_TOPJm(t_re.val[0], t_im.val[0], x1_re.val[0], x1_im.val[0], x2_re.val[0], x2_im.val[0]); -// INV_TOPJm(t_re.val[1], t_im.val[1], x1_re.val[1], x1_im.val[1], x2_re.val[1], x2_im.val[1]); - -// INV_TOPJm(t_re.val[2], t_im.val[2], y1_re.val[0], y1_im.val[0], y2_re.val[0], y2_im.val[0]); -// INV_TOPJm(t_re.val[3], t_im.val[3], y1_re.val[1], y1_im.val[1], y2_re.val[1], y2_im.val[1]); - -// INV_BOTJm_LANE(x2_re.val[0], x2_im.val[0], t_re.val[0], t_im.val[0], s2_re_im); -// INV_BOTJm_LANE(x2_re.val[1], x2_im.val[1], t_re.val[1], t_im.val[1], s2_re_im); -// INV_BOTJm_LANE(y2_re.val[0], y2_im.val[0], t_re.val[2], t_im.val[2], s2_re_im); -// INV_BOTJm_LANE(y2_re.val[1], y2_im.val[1], t_re.val[3], t_im.val[3], s2_re_im); -// } -// else -// { -// INV_TOPJ(t_re.val[0], t_im.val[0], x1_re.val[0], x1_im.val[0], x2_re.val[0], x2_im.val[0]); -// INV_TOPJ(t_re.val[1], t_im.val[1], x1_re.val[1], x1_im.val[1], x2_re.val[1], x2_im.val[1]); - -// INV_TOPJ(t_re.val[2], t_im.val[2], y1_re.val[0], y1_im.val[0], y2_re.val[0], y2_im.val[0]); -// INV_TOPJ(t_re.val[3], t_im.val[3], y1_re.val[1], y1_im.val[1], y2_re.val[1], y2_im.val[1]); - -// INV_BOTJ_LANE(x2_re.val[0], x2_im.val[0], t_re.val[0], t_im.val[0], s2_re_im); -// INV_BOTJ_LANE(x2_re.val[1], x2_im.val[1], t_re.val[1], t_im.val[1], s2_re_im); -// INV_BOTJ_LANE(y2_re.val[0], y2_im.val[0], t_re.val[2], t_im.val[2], s2_re_im); -// INV_BOTJ_LANE(y2_re.val[1], y2_im.val[1], t_re.val[3], t_im.val[3], s2_re_im); -// } - -// vstorex2(&f[j + 2*len], x2_re); -// vstorex2(&f[j + 2*len + hn], x2_im); - -// vstorex2(&f[j + 3*len], y2_re); -// vstorex2(&f[j + 3*len + hn], y2_im); - -// if (!last) -// { -// vfmuln(x1_re.val[0], x1_re.val[0], fpr_p2_tab[logn]); -// vfmuln(x1_re.val[1], x1_re.val[1], fpr_p2_tab[logn]); -// vfmuln(x1_im.val[0], x1_im.val[0], fpr_p2_tab[logn]); -// vfmuln(x1_im.val[1], x1_im.val[1], fpr_p2_tab[logn]); - -// vfmuln(y1_re.val[0], y1_re.val[0], fpr_p2_tab[logn]); -// vfmuln(y1_re.val[1], y1_re.val[1], fpr_p2_tab[logn]); -// vfmuln(y1_im.val[0], y1_im.val[0], fpr_p2_tab[logn]); -// vfmuln(y1_im.val[1], y1_im.val[1], fpr_p2_tab[logn]); -// } - -// vstorex2(&f[j], x1_re); -// vstorex2(&f[j + hn], x1_im); - -// vstorex2(&f[j + len], y1_re); -// vstorex2(&f[j + len + hn], y1_im); - -// } -// } -// } -// } - - - -static - void ZfN(iFFT_logn2)(fpr *f, const unsigned logn, const unsigned level, unsigned last) -{ - const unsigned int falcon_n = 1 << logn; - const unsigned int hn = falcon_n >> 1; - - // Total SIMD register: 26 = 16 + 8 + 2 - float64x2x4_t t_re, t_im; // 8 - float64x2x2_t x1_re, x2_re, x1_im, x2_im, - y1_re, y2_re, y1_im, y2_im; // 16 - float64x2_t s1_re_im, s2_re_im; // 2 - - const fpr *fpr_inv_tab1 = NULL, *fpr_inv_tab2 = NULL; - unsigned l, len, start, j, k1, k2; - unsigned bar = logn - 4; - - for (l = 4; l < logn - level - 1; l += 2) - { - len = 1 << l; - last -= 1; - fpr_inv_tab1 = fpr_table[bar--]; - fpr_inv_tab2 = fpr_table[bar--]; - k1 = 0; k2 = 0; - - for (start = 0; start < hn; start += 1 << (l + 2)) - { - vload(s1_re_im, &fpr_inv_tab1[k1]); - vload(s2_re_im, &fpr_inv_tab2[k2]); - k1 += 2; - k2 += 2 * ((start & 127) == 64); - if (!last) - { - vfmuln(s2_re_im, s2_re_im, fpr_p2_tab[logn]); - } - for (j = start; j < start + len; j += 4) - { - /* - Level 6 - * ( 0, 64) - ( 16, 80) - * ( 1, 65) - ( 17, 81) - * ( 0, 64) + ( 16, 80) - * ( 1, 65) + ( 17, 81) - * ( 16, 80) = @ * ( 0, 1) - * ( 17, 81) = @ * ( 0, 1) - * - * ( 2, 66) - ( 18, 82) - * ( 3, 67) - ( 19, 83) - * ( 2, 66) + ( 18, 82) - * ( 3, 67) + ( 19, 83) - * ( 18, 82) = @ * ( 0, 1) - * ( 19, 83) = @ * ( 0, 1) - * - * ( 32, 96) - ( 48, 112) - * ( 33, 97) - ( 49, 113) - * ( 32, 96) + ( 48, 112) - * ( 33, 97) + ( 49, 113) - * ( 48, 112) = j@ * ( 0, 1) - * ( 49, 113) = j@ * ( 0, 1) - * - * ( 34, 98) - ( 50, 114) - * ( 35, 99) - ( 51, 115) - * ( 34, 98) + ( 50, 114) - * ( 35, 99) + ( 51, 115) - * ( 50, 114) = j@ * ( 0, 1) - * ( 51, 115) = j@ * ( 0, 1) - */ - // x1: 0 -> 4 | 64 -> 67 - // y1: 16 -> 19 | 80 -> 81 - // x2: 32 -> 35 | 96 -> 99 - // y2: 48 -> 51 | 112 -> 115 - vloadx2(x1_re, &f[j]); - vloadx2(x1_im, &f[j + hn]); - vloadx2(y1_re, &f[j + len]); - vloadx2(y1_im, &f[j + len + hn]); - - INV_TOPJ (t_re.val[0], t_im.val[0], x1_re.val[0], x1_im.val[0], y1_re.val[0], y1_im.val[0]); - INV_TOPJ (t_re.val[1], t_im.val[1], x1_re.val[1], x1_im.val[1], y1_re.val[1], y1_im.val[1]); - - vloadx2(x2_re, &f[j + 2*len]); - vloadx2(x2_im, &f[j + 2*len + hn]); - vloadx2(y2_re, &f[j + 3*len]); - vloadx2(y2_im, &f[j + 3*len + hn]); - - INV_TOPJm(t_re.val[2], t_im.val[2], x2_re.val[0], x2_im.val[0], y2_re.val[0], y2_im.val[0]); - INV_TOPJm(t_re.val[3], t_im.val[3], x2_re.val[1], x2_im.val[1], y2_re.val[1], y2_im.val[1]); - - INV_BOTJ_LANE (y1_re.val[0], y1_im.val[0], t_re.val[0], t_im.val[0], s1_re_im); - INV_BOTJ_LANE (y1_re.val[1], y1_im.val[1], t_re.val[1], t_im.val[1], s1_re_im); - - INV_BOTJm_LANE(y2_re.val[0], y2_im.val[0], t_re.val[2], t_im.val[2], s1_re_im); - INV_BOTJm_LANE(y2_re.val[1], y2_im.val[1], t_re.val[3], t_im.val[3], s1_re_im); - /* - * Level 7 - * ( 0, 64) - ( 32, 96) - * ( 1, 65) - ( 33, 97) - * ( 0, 64) + ( 32, 96) - * ( 1, 65) + ( 33, 97) - * ( 32, 96) = @ * ( 0, 1) - * ( 33, 97) = @ * ( 0, 1) - * - * ( 2, 66) - ( 34, 98) - * ( 3, 67) - ( 35, 99) - * ( 2, 66) + ( 34, 98) - * ( 3, 67) + ( 35, 99) - * ( 34, 98) = @ * ( 0, 1) - * ( 35, 99) = @ * ( 0, 1) - * ---- - * ( 16, 80) - ( 48, 112) - * ( 17, 81) - ( 49, 113) - * ( 16, 80) + ( 48, 112) - * ( 17, 81) + ( 49, 113) - * ( 48, 112) = @ * ( 0, 1) - * ( 49, 113) = @ * ( 0, 1) - * - * ( 18, 82) - ( 50, 114) - * ( 19, 83) - ( 51, 115) - * ( 18, 82) + ( 50, 114) - * ( 19, 83) + ( 51, 115) - * ( 50, 114) = @ * ( 0, 1) - * ( 51, 115) = @ * ( 0, 1) - */ - - - INV_TOPJ(t_re.val[0], t_im.val[0], x1_re.val[0], x1_im.val[0], x2_re.val[0], x2_im.val[0]); - INV_TOPJ(t_re.val[1], t_im.val[1], x1_re.val[1], x1_im.val[1], x2_re.val[1], x2_im.val[1]); - - INV_TOPJ(t_re.val[2], t_im.val[2], y1_re.val[0], y1_im.val[0], y2_re.val[0], y2_im.val[0]); - INV_TOPJ(t_re.val[3], t_im.val[3], y1_re.val[1], y1_im.val[1], y2_re.val[1], y2_im.val[1]); - - INV_BOTJ_LANE(x2_re.val[0], x2_im.val[0], t_re.val[0], t_im.val[0], s2_re_im); - INV_BOTJ_LANE(x2_re.val[1], x2_im.val[1], t_re.val[1], t_im.val[1], s2_re_im); - INV_BOTJ_LANE(y2_re.val[0], y2_im.val[0], t_re.val[2], t_im.val[2], s2_re_im); - INV_BOTJ_LANE(y2_re.val[1], y2_im.val[1], t_re.val[3], t_im.val[3], s2_re_im); - - vstorex2(&f[j + 2*len], x2_re); - vstorex2(&f[j + 2*len + hn], x2_im); - - vstorex2(&f[j + 3*len], y2_re); - vstorex2(&f[j + 3*len + hn], y2_im); - - if (!last) - { - vfmuln(x1_re.val[0], x1_re.val[0], fpr_p2_tab[logn]); - vfmuln(x1_re.val[1], x1_re.val[1], fpr_p2_tab[logn]); - vfmuln(x1_im.val[0], x1_im.val[0], fpr_p2_tab[logn]); - vfmuln(x1_im.val[1], x1_im.val[1], fpr_p2_tab[logn]); - - vfmuln(y1_re.val[0], y1_re.val[0], fpr_p2_tab[logn]); - vfmuln(y1_re.val[1], y1_re.val[1], fpr_p2_tab[logn]); - vfmuln(y1_im.val[0], y1_im.val[0], fpr_p2_tab[logn]); - vfmuln(y1_im.val[1], y1_im.val[1], fpr_p2_tab[logn]); - } - - vstorex2(&f[j], x1_re); - vstorex2(&f[j + hn], x1_im); - - vstorex2(&f[j + len], y1_re); - vstorex2(&f[j + len + hn], y1_im); - - } - // - start += 1 << (l + 2); - if (start >= hn) break; - - vload(s1_re_im, &fpr_inv_tab1[k1]); - vload(s2_re_im, &fpr_inv_tab2[k2]); - k1 += 2; - k2 += 2 * ((start & 127) == 64); - if (!last) - { - vfmuln(s2_re_im, s2_re_im, fpr_p2_tab[logn]); - } - - for (j = start; j < start + len; j += 4) - { - /* - Level 6 - * ( 0, 64) - ( 16, 80) - * ( 1, 65) - ( 17, 81) - * ( 0, 64) + ( 16, 80) - * ( 1, 65) + ( 17, 81) - * ( 16, 80) = @ * ( 0, 1) - * ( 17, 81) = @ * ( 0, 1) - * - * ( 2, 66) - ( 18, 82) - * ( 3, 67) - ( 19, 83) - * ( 2, 66) + ( 18, 82) - * ( 3, 67) + ( 19, 83) - * ( 18, 82) = @ * ( 0, 1) - * ( 19, 83) = @ * ( 0, 1) - * - * ( 32, 96) - ( 48, 112) - * ( 33, 97) - ( 49, 113) - * ( 32, 96) + ( 48, 112) - * ( 33, 97) + ( 49, 113) - * ( 48, 112) = j@ * ( 0, 1) - * ( 49, 113) = j@ * ( 0, 1) - * - * ( 34, 98) - ( 50, 114) - * ( 35, 99) - ( 51, 115) - * ( 34, 98) + ( 50, 114) - * ( 35, 99) + ( 51, 115) - * ( 50, 114) = j@ * ( 0, 1) - * ( 51, 115) = j@ * ( 0, 1) - */ - // x1: 0 -> 4 | 64 -> 67 - // y1: 16 -> 19 | 80 -> 81 - // x2: 32 -> 35 | 96 -> 99 - // y2: 48 -> 51 | 112 -> 115 - vloadx2(x1_re, &f[j]); - vloadx2(x1_im, &f[j + hn]); - vloadx2(y1_re, &f[j + len]); - vloadx2(y1_im, &f[j + len + hn]); - - INV_TOPJ (t_re.val[0], t_im.val[0], x1_re.val[0], x1_im.val[0], y1_re.val[0], y1_im.val[0]); - INV_TOPJ (t_re.val[1], t_im.val[1], x1_re.val[1], x1_im.val[1], y1_re.val[1], y1_im.val[1]); - - vloadx2(x2_re, &f[j + 2*len]); - vloadx2(x2_im, &f[j + 2*len + hn]); - vloadx2(y2_re, &f[j + 3*len]); - vloadx2(y2_im, &f[j + 3*len + hn]); - - INV_TOPJm(t_re.val[2], t_im.val[2], x2_re.val[0], x2_im.val[0], y2_re.val[0], y2_im.val[0]); - INV_TOPJm(t_re.val[3], t_im.val[3], x2_re.val[1], x2_im.val[1], y2_re.val[1], y2_im.val[1]); - - INV_BOTJ_LANE (y1_re.val[0], y1_im.val[0], t_re.val[0], t_im.val[0], s1_re_im); - INV_BOTJ_LANE (y1_re.val[1], y1_im.val[1], t_re.val[1], t_im.val[1], s1_re_im); - - INV_BOTJm_LANE(y2_re.val[0], y2_im.val[0], t_re.val[2], t_im.val[2], s1_re_im); - INV_BOTJm_LANE(y2_re.val[1], y2_im.val[1], t_re.val[3], t_im.val[3], s1_re_im); - /* - * Level 7 - * ( 0, 64) - ( 32, 96) - * ( 1, 65) - ( 33, 97) - * ( 0, 64) + ( 32, 96) - * ( 1, 65) + ( 33, 97) - * ( 32, 96) = @ * ( 0, 1) - * ( 33, 97) = @ * ( 0, 1) - * - * ( 2, 66) - ( 34, 98) - * ( 3, 67) - ( 35, 99) - * ( 2, 66) + ( 34, 98) - * ( 3, 67) + ( 35, 99) - * ( 34, 98) = @ * ( 0, 1) - * ( 35, 99) = @ * ( 0, 1) - * ---- - * ( 16, 80) - ( 48, 112) - * ( 17, 81) - ( 49, 113) - * ( 16, 80) + ( 48, 112) - * ( 17, 81) + ( 49, 113) - * ( 48, 112) = @ * ( 0, 1) - * ( 49, 113) = @ * ( 0, 1) - * - * ( 18, 82) - ( 50, 114) - * ( 19, 83) - ( 51, 115) - * ( 18, 82) + ( 50, 114) - * ( 19, 83) + ( 51, 115) - * ( 50, 114) = @ * ( 0, 1) - * ( 51, 115) = @ * ( 0, 1) - */ - - INV_TOPJm(t_re.val[0], t_im.val[0], x1_re.val[0], x1_im.val[0], x2_re.val[0], x2_im.val[0]); - INV_TOPJm(t_re.val[1], t_im.val[1], x1_re.val[1], x1_im.val[1], x2_re.val[1], x2_im.val[1]); - - INV_TOPJm(t_re.val[2], t_im.val[2], y1_re.val[0], y1_im.val[0], y2_re.val[0], y2_im.val[0]); - INV_TOPJm(t_re.val[3], t_im.val[3], y1_re.val[1], y1_im.val[1], y2_re.val[1], y2_im.val[1]); - - INV_BOTJm_LANE(x2_re.val[0], x2_im.val[0], t_re.val[0], t_im.val[0], s2_re_im); - INV_BOTJm_LANE(x2_re.val[1], x2_im.val[1], t_re.val[1], t_im.val[1], s2_re_im); - INV_BOTJm_LANE(y2_re.val[0], y2_im.val[0], t_re.val[2], t_im.val[2], s2_re_im); - INV_BOTJm_LANE(y2_re.val[1], y2_im.val[1], t_re.val[3], t_im.val[3], s2_re_im); - - vstorex2(&f[j + 2*len], x2_re); - vstorex2(&f[j + 2*len + hn], x2_im); - - vstorex2(&f[j + 3*len], y2_re); - vstorex2(&f[j + 3*len + hn], y2_im); - - if (!last) - { - vfmuln(x1_re.val[0], x1_re.val[0], fpr_p2_tab[logn]); - vfmuln(x1_re.val[1], x1_re.val[1], fpr_p2_tab[logn]); - vfmuln(x1_im.val[0], x1_im.val[0], fpr_p2_tab[logn]); - vfmuln(x1_im.val[1], x1_im.val[1], fpr_p2_tab[logn]); - - vfmuln(y1_re.val[0], y1_re.val[0], fpr_p2_tab[logn]); - vfmuln(y1_re.val[1], y1_re.val[1], fpr_p2_tab[logn]); - vfmuln(y1_im.val[0], y1_im.val[0], fpr_p2_tab[logn]); - vfmuln(y1_im.val[1], y1_im.val[1], fpr_p2_tab[logn]); - } - - vstorex2(&f[j], x1_re); - vstorex2(&f[j + hn], x1_im); - - vstorex2(&f[j + len], y1_re); - vstorex2(&f[j + len + hn], y1_im); - - } - // - } - } -} - - -/* -* Support logn from [1, 10] -* Can be easily extended to logn > 10 -*/ -void ZfN(iFFT)(fpr *f, const unsigned logn) -{ - const unsigned level = (logn - 5) & 1; - - switch (logn) - { - case 2: - ZfN(iFFT_log2)(f); - break; - - case 3: - ZfN(iFFT_log3)(f); - break; - - case 4: - ZfN(iFFT_log4)(f); - break; - - case 5: - ZfN(iFFT_log5)(f, 5, 1); - break; - - case 6: - ZfN(iFFT_log5)(f, logn, 0); - ZfN(iFFT_logn1)(f, logn, 1); - break; - - case 7: - case 9: - ZfN(iFFT_log5)(f, logn, 0); - ZfN(iFFT_logn2)(f, logn, level, 1); - break; - - case 8: - case 10: - ZfN(iFFT_log5)(f, logn, 0); - ZfN(iFFT_logn2)(f, logn, level, 0); - ZfN(iFFT_logn1)(f, logn, 1); - break; - - default: - break; - } -} -#endif - -// generic fft stuff - -#include "../commons_private.h" -#include "reim_fft_internal.h" -#include "reim_fft_private.h" - -void reim_ctwiddle(double* ra, double* ia, double* rb, double* ib, double omre, double omim); -void reim_citwiddle(double* ra, double* ia, double* rb, double* ib, double omre, double omim); - -void reim_fft16_ref(double* dre, double* dim, const void* pom); -void fill_reim_fft16_omegas(const double entry_pwr, double** omg); -void reim_fft8_ref(double* dre, double* dim, const void* pom); -void reim_twiddle_fft_ref(uint64_t h, double* re, double* im, double om[2]); -void fill_reim_twiddle_fft_ref(const double s, double** omg); -void reim_bitwiddle_fft_ref(uint64_t h, double* re, double* im, double om[4]); -void fill_reim_bitwiddle_fft_ref(const double s, double** omg); -void reim_fft_rec_16_ref(uint64_t m, double* re, double* im, double** omg); -void fill_reim_fft_rec_16_omegas(uint64_t m, double entry_pwr, double** omg); - -void fill_reim_twiddle_fft_omegas_ref(const double rs0, double** omg) { - (*omg)[0] = cos(2 * M_PI * rs0); - (*omg)[1] = sin(2 * M_PI * rs0); - *omg += 2; -} - -void fill_reim_bitwiddle_fft_omegas_ref(const double rs0, double** omg) { - double rs1 = 2. * rs0; - (*omg)[0] = cos(2 * M_PI * rs1); - (*omg)[1] = sin(2 * M_PI * rs1); - (*omg)[2] = cos(2 * M_PI * rs0); - (*omg)[3] = sin(2 * M_PI * rs0); - *omg += 4; -} - -#define reim_fft16_f reim_fft16_neon -#define reim_fft16_pom_offset 16 -#define fill_reim_fft16_omegas_f fill_reim_fft16_omegas_neon -// currently, m=4 uses the ref implem (corner case with low impact) TODO!! -#define reim_fft8_f reim_fft8_ref -#define reim_fft8_pom_offset 8 -#define fill_reim_fft8_omegas_f fill_reim_fft8_omegas -// currently, m=4 uses the ref implem (corner case with low impact) TODO!! -#define reim_fft4_f reim_fft4_ref -#define reim_fft4_pom_offset 4 -#define fill_reim_fft4_omegas_f fill_reim_fft4_omegas -// m = 2 will use the ref implem, since intrinsics don't provide any speed-up -#define reim_fft2_f reim_fft2_ref -#define reim_fft2_pom_offset 2 -#define fill_reim_fft2_omegas_f fill_reim_fft2_omegas - -// neon twiddle use the same omegas layout as the ref implem -#define reim_twiddle_fft_f reim_twiddle_fft_neon -#define reim_twiddle_fft_pom_offset 2 -#define fill_reim_twiddle_fft_omegas_f fill_reim_twiddle_fft_omegas_ref - -// neon bi-twiddle use the same omegas layout as the ref implem -#define reim_bitwiddle_fft_f reim_bitwiddle_fft_neon -#define reim_bitwiddle_fft_pom_offset 4 -#define fill_reim_bitwiddle_fft_omegas_f fill_reim_bitwiddle_fft_omegas_ref - -// template functions to produce -#define reim_fft_bfs_16_f reim_fft_bfs_16_neon -#define fill_reim_fft_bfs_16_omegas_f fill_reim_fft_bfs_16_omegas_neon -#define reim_fft_rec_16_f reim_fft_rec_16_neon -#define fill_reim_fft_rec_16_omegas_f fill_reim_fft_rec_16_omegas_neon -#define reim_fft_f reim_fft_neon -#define fill_reim_fft_omegas_f fill_reim_fft_omegas_neon - -#include "reim_fft_core_template.h" - -EXPORT REIM_FFT_PRECOMP* new_reim_fft_precomp_neon(uint32_t m, uint32_t num_buffers) { - const uint64_t OMG_SPACE = ceilto64b(2 * m * sizeof(double)); - const uint64_t BUF_SIZE = ceilto64b(2 * m * sizeof(double)); - void* reps = malloc(sizeof(REIM_FFT_PRECOMP) // base - + 63 // padding - + OMG_SPACE // tables //TODO 16? - + num_buffers * BUF_SIZE // buffers - ); - uint64_t aligned_addr = ceilto64b((uint64_t)(reps) + sizeof(REIM_FFT_PRECOMP)); - REIM_FFT_PRECOMP* r = (REIM_FFT_PRECOMP*)reps; - r->m = m; - r->buf_size = BUF_SIZE; - r->powomegas = (double*)aligned_addr; - r->aligned_buffers = (void*)(aligned_addr + OMG_SPACE); - // fill in powomegas - double* omg = (double*)r->powomegas; - fill_reim_fft_omegas_f(m, 0.25, &omg); - if (((uint64_t)omg) - aligned_addr > OMG_SPACE) abort(); - // dispatch the right implementation - //{ - // if (CPU_SUPPORTS("fma")) { - // r->function = reim_fft_avx2_fma; - // } else { - // r->function = reim_fft_ref; - // } - //} - r->function = reim_fft_f; - return reps; -}
D spqlios/lib/spqlios/reim/reim_fft_private.h

@@ -1,101 +0,0 @@

-#ifndef SPQLIOS_REIM_FFT_PRIVATE_H -#define SPQLIOS_REIM_FFT_PRIVATE_H - -#include "../commons_private.h" -#include "reim_fft.h" - -#define STATIC_ASSERT(condition) (void)sizeof(char[-1 + 2 * !!(condition)]) - -typedef struct reim_twiddle_precomp REIM_FFTVEC_TWIDDLE_PRECOMP; -typedef struct reim_bitwiddle_precomp REIM_FFTVEC_BITWIDDLE_PRECOMP; - -typedef void (*FFT_FUNC)(const REIM_FFT_PRECOMP*, double*); -typedef void (*IFFT_FUNC)(const REIM_IFFT_PRECOMP*, double*); -typedef void (*FFTVEC_MUL_FUNC)(const REIM_FFTVEC_MUL_PRECOMP*, double*, const double*, const double*); -typedef void (*FFTVEC_ADDMUL_FUNC)(const REIM_FFTVEC_ADDMUL_PRECOMP*, double*, const double*, const double*); - -typedef void (*FROM_ZNX32_FUNC)(const REIM_FROM_ZNX32_PRECOMP*, void*, const int32_t*); -typedef void (*FROM_ZNX64_FUNC)(const REIM_FROM_ZNX64_PRECOMP*, void*, const int64_t*); -typedef void (*FROM_TNX32_FUNC)(const REIM_FROM_TNX32_PRECOMP*, void*, const int32_t*); -typedef void (*TO_TNX32_FUNC)(const REIM_TO_TNX32_PRECOMP*, int32_t*, const void*); -typedef void (*TO_TNX_FUNC)(const REIM_TO_TNX_PRECOMP*, double*, const double*); -typedef void (*TO_ZNX64_FUNC)(const REIM_TO_ZNX64_PRECOMP*, int64_t*, const void*); -typedef void (*FFTVEC_TWIDDLE_FUNC)(const REIM_FFTVEC_TWIDDLE_PRECOMP*, void*, const void*, const void*); -typedef void (*FFTVEC_BITWIDDLE_FUNC)(const REIM_FFTVEC_BITWIDDLE_PRECOMP*, void*, uint64_t, const void*); - -typedef struct reim_fft_precomp { - FFT_FUNC function; - int64_t m; ///< complex dimension warning: reim uses n=2N=4m - uint64_t buf_size; ///< size of aligned_buffers (multiple of 64B) - double* powomegas; ///< 64B aligned - void* aligned_buffers; ///< 64B aligned -} REIM_FFT_PRECOMP; - -typedef struct reim_ifft_precomp { - IFFT_FUNC function; - int64_t m; // warning: reim uses n=2N=4m - uint64_t buf_size; ///< size of aligned_buffers (multiple of 64B) - double* powomegas; - void* aligned_buffers; -} REIM_IFFT_PRECOMP; - -typedef struct reim_mul_precomp { - FFTVEC_MUL_FUNC function; - int64_t m; -} REIM_FFTVEC_MUL_PRECOMP; - -typedef struct reim_addmul_precomp { - FFTVEC_ADDMUL_FUNC function; - int64_t m; -} REIM_FFTVEC_ADDMUL_PRECOMP; - - -struct reim_from_znx32_precomp { - FROM_ZNX32_FUNC function; - int64_t m; -}; - -struct reim_from_znx64_precomp { - FROM_ZNX64_FUNC function; - int64_t m; -}; - -struct reim_from_tnx32_precomp { - FROM_TNX32_FUNC function; - int64_t m; -}; - -struct reim_to_tnx32_precomp { - TO_TNX32_FUNC function; - int64_t m; - double divisor; -}; - -struct reim_to_tnx_precomp { - TO_TNX_FUNC function; - int64_t m; - double divisor; - uint32_t log2overhead; - double add_cst; - uint64_t mask_and; - uint64_t mask_or; - double sub_cst; -}; - -struct reim_to_znx64_precomp { - TO_ZNX64_FUNC function; - int64_t m; - double divisor; -}; - -struct reim_twiddle_precomp { - FFTVEC_TWIDDLE_FUNC function; - int64_t m; -}; - -struct reim_bitwiddle_precomp { - FFTVEC_BITWIDDLE_FUNC function; - int64_t m; -}; - -#endif // SPQLIOS_REIM_FFT_PRIVATE_H
D spqlios/lib/spqlios/reim/reim_fft_ref.c

@@ -1,437 +0,0 @@

-#include "../commons_private.h" -#include "reim_fft_internal.h" -#include "reim_fft_private.h" - -EXPORT void reim_fft_simple(uint32_t m, void* data) { - static REIM_FFT_PRECOMP* p[31] = {0}; - REIM_FFT_PRECOMP** f = p + log2m(m); - if (!*f) *f = new_reim_fft_precomp(m, 0); - (*f)->function(*f, data); -} - -EXPORT void reim_ifft_simple(uint32_t m, void* data) { - static REIM_IFFT_PRECOMP* p[31] = {0}; - REIM_IFFT_PRECOMP** f = p + log2m(m); - if (!*f) *f = new_reim_ifft_precomp(m, 0); - (*f)->function(*f, data); -} - -EXPORT void reim_fftvec_mul_simple(uint32_t m, void* r, const void* a, const void* b) { - static REIM_FFTVEC_MUL_PRECOMP* p[31] = {0}; - REIM_FFTVEC_MUL_PRECOMP** f = p + log2m(m); - if (!*f) *f = new_reim_fftvec_mul_precomp(m); - (*f)->function(*f, r, a, b); -} - -EXPORT void reim_fftvec_addmul_simple(uint32_t m, void* r, const void* a, const void* b) { - static REIM_FFTVEC_ADDMUL_PRECOMP* p[31] = {0}; - REIM_FFTVEC_ADDMUL_PRECOMP** f = p + log2m(m); - if (!*f) *f = new_reim_fftvec_addmul_precomp(m); - (*f)->function(*f, r, a, b); -} - -void reim_ctwiddle(double* ra, double* ia, double* rb, double* ib, double omre, double omim) { - double newrt = *rb * omre - *ib * omim; - double newit = *rb * omim + *ib * omre; - *rb = *ra - newrt; - *ib = *ia - newit; - *ra = *ra + newrt; - *ia = *ia + newit; -} - -// i (omre + i omim) = -omim + i omre -void reim_citwiddle(double* ra, double* ia, double* rb, double* ib, double omre, double omim) { - double newrt = -*rb * omim - *ib * omre; - double newit = *rb * omre - *ib * omim; - *rb = *ra - newrt; - *ib = *ia - newit; - *ra = *ra + newrt; - *ia = *ia + newit; -} - -void reim_fft16_ref(double* dre, double* dim, const void* pom) { - const double* om = (const double*)pom; - { - double omre = om[0]; - double omim = om[1]; - reim_ctwiddle(&dre[0], &dim[0], &dre[8], &dim[8], omre, omim); - reim_ctwiddle(&dre[1], &dim[1], &dre[9], &dim[9], omre, omim); - reim_ctwiddle(&dre[2], &dim[2], &dre[10], &dim[10], omre, omim); - reim_ctwiddle(&dre[3], &dim[3], &dre[11], &dim[11], omre, omim); - reim_ctwiddle(&dre[4], &dim[4], &dre[12], &dim[12], omre, omim); - reim_ctwiddle(&dre[5], &dim[5], &dre[13], &dim[13], omre, omim); - reim_ctwiddle(&dre[6], &dim[6], &dre[14], &dim[14], omre, omim); - reim_ctwiddle(&dre[7], &dim[7], &dre[15], &dim[15], omre, omim); - } - { - double omre = om[2]; - double omim = om[3]; - reim_ctwiddle(&dre[0], &dim[0], &dre[4], &dim[4], omre, omim); - reim_ctwiddle(&dre[1], &dim[1], &dre[5], &dim[5], omre, omim); - reim_ctwiddle(&dre[2], &dim[2], &dre[6], &dim[6], omre, omim); - reim_ctwiddle(&dre[3], &dim[3], &dre[7], &dim[7], omre, omim); - reim_citwiddle(&dre[8], &dim[8], &dre[12], &dim[12], omre, omim); - reim_citwiddle(&dre[9], &dim[9], &dre[13], &dim[13], omre, omim); - reim_citwiddle(&dre[10], &dim[10], &dre[14], &dim[14], omre, omim); - reim_citwiddle(&dre[11], &dim[11], &dre[15], &dim[15], omre, omim); - } - { - double omare = om[4]; - double omaim = om[5]; - double ombre = om[6]; - double ombim = om[7]; - reim_ctwiddle(&dre[0], &dim[0], &dre[2], &dim[2], omare, omaim); - reim_ctwiddle(&dre[1], &dim[1], &dre[3], &dim[3], omare, omaim); - reim_citwiddle(&dre[4], &dim[4], &dre[6], &dim[6], omare, omaim); - reim_citwiddle(&dre[5], &dim[5], &dre[7], &dim[7], omare, omaim); - reim_ctwiddle(&dre[8], &dim[8], &dre[10], &dim[10], ombre, ombim); - reim_ctwiddle(&dre[9], &dim[9], &dre[11], &dim[11], ombre, ombim); - reim_citwiddle(&dre[12], &dim[12], &dre[14], &dim[14], ombre, ombim); - reim_citwiddle(&dre[13], &dim[13], &dre[15], &dim[15], ombre, ombim); - } - { - double omare = om[8]; - double ombre = om[9]; - double omcre = om[10]; - double omdre = om[11]; - double omaim = om[12]; - double ombim = om[13]; - double omcim = om[14]; - double omdim = om[15]; - reim_ctwiddle(&dre[0], &dim[0], &dre[1], &dim[1], omare, omaim); - reim_citwiddle(&dre[2], &dim[2], &dre[3], &dim[3], omare, omaim); - reim_ctwiddle(&dre[4], &dim[4], &dre[5], &dim[5], ombre, ombim); - reim_citwiddle(&dre[6], &dim[6], &dre[7], &dim[7], ombre, ombim); - reim_ctwiddle(&dre[8], &dim[8], &dre[9], &dim[9], omcre, omcim); - reim_citwiddle(&dre[10], &dim[10], &dre[11], &dim[11], omcre, omcim); - reim_ctwiddle(&dre[12], &dim[12], &dre[13], &dim[13], omdre, omdim); - reim_citwiddle(&dre[14], &dim[14], &dre[15], &dim[15], omdre, omdim); - } -} - -void fill_reim_fft16_omegas(const double entry_pwr, double** omg) { - const double j_pow = 1. / 8.; - const double k_pow = 1. / 16.; - const double pin = entry_pwr / 2.; - const double pin_2 = entry_pwr / 4.; - const double pin_4 = entry_pwr / 8.; - const double pin_8 = entry_pwr / 16.; - // 0 and 1 are real and imag of om - (*omg)[0] = cos(2. * M_PI * pin); - (*omg)[1] = sin(2. * M_PI * pin); - // 2 and 3 are real and imag of om^1/2 - (*omg)[2] = cos(2. * M_PI * (pin_2)); - (*omg)[3] = sin(2. * M_PI * (pin_2)); - // (4,5) and (6,7) are real and imag of om^1/4 and j.om^1/4 - (*omg)[4] = cos(2. * M_PI * (pin_4)); - (*omg)[5] = sin(2. * M_PI * (pin_4)); - (*omg)[6] = cos(2. * M_PI * (pin_4 + j_pow)); - (*omg)[7] = sin(2. * M_PI * (pin_4 + j_pow)); - // ((8,9,10,11),(12,13,14,15)) are 4 reals then 4 imag of om^1/8*(1,k,j,kj) - (*omg)[8] = cos(2. * M_PI * (pin_8)); - (*omg)[9] = cos(2. * M_PI * (pin_8 + j_pow)); - (*omg)[10] = cos(2. * M_PI * (pin_8 + k_pow)); - (*omg)[11] = cos(2. * M_PI * (pin_8 + j_pow + k_pow)); - (*omg)[12] = sin(2. * M_PI * (pin_8)); - (*omg)[13] = sin(2. * M_PI * (pin_8 + j_pow)); - (*omg)[14] = sin(2. * M_PI * (pin_8 + k_pow)); - (*omg)[15] = sin(2. * M_PI * (pin_8 + j_pow + k_pow)); - *omg += 16; -} - -void reim_fft8_ref(double* dre, double* dim, const void* pom) { - const double* om = (const double*)pom; - { - double omre = om[0]; - double omim = om[1]; - reim_ctwiddle(&dre[0], &dim[0], &dre[4], &dim[4], omre, omim); - reim_ctwiddle(&dre[1], &dim[1], &dre[5], &dim[5], omre, omim); - reim_ctwiddle(&dre[2], &dim[2], &dre[6], &dim[6], omre, omim); - reim_ctwiddle(&dre[3], &dim[3], &dre[7], &dim[7], omre, omim); - } - { - double omare = om[2]; - double omaim = om[3]; - reim_ctwiddle(&dre[0], &dim[0], &dre[2], &dim[2], omare, omaim); - reim_ctwiddle(&dre[1], &dim[1], &dre[3], &dim[3], omare, omaim); - reim_citwiddle(&dre[4], &dim[4], &dre[6], &dim[6], omare, omaim); - reim_citwiddle(&dre[5], &dim[5], &dre[7], &dim[7], omare, omaim); - } - { - double omare = om[4]; - double ombre = om[5]; - double omaim = om[6]; - double ombim = om[7]; - reim_ctwiddle(&dre[0], &dim[0], &dre[1], &dim[1], omare, omaim); - reim_citwiddle(&dre[2], &dim[2], &dre[3], &dim[3], omare, omaim); - reim_ctwiddle(&dre[4], &dim[4], &dre[5], &dim[5], ombre, ombim); - reim_citwiddle(&dre[6], &dim[6], &dre[7], &dim[7], ombre, ombim); - } -} - -void fill_reim_fft8_omegas(const double entry_pwr, double** omg) { - const double j_pow = 1. / 8.; - const double pin = entry_pwr / 2.; - const double pin_2 = entry_pwr / 4.; - const double pin_4 = entry_pwr / 8.; - // 0 and 1 are real and imag of om - (*omg)[0] = cos(2. * M_PI * pin); - (*omg)[1] = sin(2. * M_PI * pin); - // 2 and 3 are real and imag of om^1/2 - (*omg)[2] = cos(2. * M_PI * (pin_2)); - (*omg)[3] = sin(2. * M_PI * (pin_2)); - // (4,5) and (6,7) are real and imag of om^1/4 and j.om^1/4 - (*omg)[4] = cos(2. * M_PI * (pin_4)); - (*omg)[5] = cos(2. * M_PI * (pin_4 + j_pow)); - (*omg)[6] = sin(2. * M_PI * (pin_4)); - (*omg)[7] = sin(2. * M_PI * (pin_4 + j_pow)); - *omg += 8; -} - -void reim_fft4_ref(double* dre, double* dim, const void* pom) { - const double* om = (const double*)pom; - { - double omare = om[0]; - double omaim = om[1]; - reim_ctwiddle(&dre[0], &dim[0], &dre[2], &dim[2], omare, omaim); - reim_ctwiddle(&dre[1], &dim[1], &dre[3], &dim[3], omare, omaim); - } - { - double omare = om[2]; - double omaim = om[3]; - reim_ctwiddle(&dre[0], &dim[0], &dre[1], &dim[1], omare, omaim); - reim_citwiddle(&dre[2], &dim[2], &dre[3], &dim[3], omare, omaim); - } -} - -void fill_reim_fft4_omegas(const double entry_pwr, double** omg) { - const double pin = entry_pwr / 2.; - const double pin_2 = entry_pwr / 4.; - // 0 and 1 are real and imag of om - (*omg)[0] = cos(2. * M_PI * pin); - (*omg)[1] = sin(2. * M_PI * pin); - // 2 and 3 are real and imag of om^1/2 - (*omg)[2] = cos(2. * M_PI * (pin_2)); - (*omg)[3] = sin(2. * M_PI * (pin_2)); - *omg += 4; -} - -void reim_fft2_ref(double* dre, double* dim, const void* pom) { - const double* om = (const double*)pom; - { - double omare = om[0]; - double omaim = om[1]; - reim_ctwiddle(&dre[0], &dim[0], &dre[1], &dim[1], omare, omaim); - } -} - -void fill_reim_fft2_omegas(const double entry_pwr, double** omg) { - const double pin = entry_pwr / 2.; - // 0 and 1 are real and imag of om - (*omg)[0] = cos(2. * M_PI * pin); - (*omg)[1] = sin(2. * M_PI * pin); - *omg += 2; -} - -void reim_twiddle_fft_ref(uint64_t h, double* re, double* im, double om[2]) { - for (uint64_t i=0; i<h; ++i) { - reim_ctwiddle(&re[i],&im[i],&re[h+i],&im[h+i], om[0], om[1]); - } -} - -void reim_bitwiddle_fft_ref(uint64_t h, double* re, double* im, double om[4]) { - double* r0 = re; - double* r1 = re + h; - double* r2 = re + 2*h; - double* r3 = re + 3*h; - double* i0 = im; - double* i1 = im + h; - double* i2 = im + 2*h; - double* i3 = im + 3*h; - for (uint64_t i=0; i<h; ++i) { - reim_ctwiddle(&r0[i],&i0[i],&r2[i],&i2[i], om[0], om[1]); - reim_ctwiddle(&r1[i],&i1[i],&r3[i],&i3[i], om[0], om[1]); - } - for (uint64_t i=0; i<h; ++i) { - reim_ctwiddle(&r0[i],&i0[i],&r1[i],&i1[i], om[2], om[3]); - reim_citwiddle(&r2[i],&i2[i],&r3[i],&i3[i], om[2], om[3]); - } -} - -void reim_fft_bfs_16_ref(uint64_t m, double* re, double* im, double** omg) { - uint64_t log2m = log2(m); - uint64_t mm = m; - if (log2m % 2 != 0) { - uint64_t h = mm >> 1; - // do the first twiddle iteration normally - reim_twiddle_fft_ref(h, re, im, *omg); - *omg += 2; - mm = h; - } - while (mm > 16) { - uint64_t h = mm >> 2; - for (uint64_t off = 0; off < m; off += mm) { - reim_bitwiddle_fft_ref(h, re + off, im + off, *omg); - *omg += 4; - } - mm = h; - } - if (mm!=16) abort(); // bug! - for (uint64_t off = 0; off < m; off += 16) { - reim_fft16_ref(re+off, im+off, *omg); - *omg += 16; - } -} - -void fill_reim_fft_bfs_16_omegas(uint64_t m, double entry_pwr, double** omg) { - uint64_t log2m = log2(m); - uint64_t mm = m; - double ss = entry_pwr; - if (log2m % 2 != 0) { - uint64_t h = mm >> 1; - double s = ss / 2.; - // do the first twiddle iteration normally - (*omg)[0] = cos(2 * M_PI * s); - (*omg)[1] = sin(2 * M_PI * s); - *omg += 2; - mm = h; - ss = s; - } - while (mm > 16) { - uint64_t h = mm >> 2; - double s = ss / 4.; - for (uint64_t off = 0; off < m; off += mm) { - double rs0 = s + fracrevbits(off/mm) / 4.; - double rs1 = 2. * rs0; - (*omg)[0] = cos(2 * M_PI * rs1); - (*omg)[1] = sin(2 * M_PI * rs1); - (*omg)[2] = cos(2 * M_PI * rs0); - (*omg)[3] = sin(2 * M_PI * rs0); - *omg += 4; - } - mm = h; - ss = s; - } - if (mm!=16) abort(); // bug! - for (uint64_t off = 0; off < m; off += 16) { - double s = ss + fracrevbits(off/16); - fill_reim_fft16_omegas(s, omg); - } -} - -void reim_fft_rec_16_ref(uint64_t m, double* re, double* im, double** omg) { - if (m <= 2048) return reim_fft_bfs_16_ref(m, re, im, omg); - const uint32_t h = m / 2; - reim_twiddle_fft_ref(h, re, im, *omg); - *omg += 2; - reim_fft_rec_16_ref(h, re, im, omg); - reim_fft_rec_16_ref(h, re + h, im + h, omg); -} - -void fill_reim_fft_rec_16_omegas(uint64_t m, double entry_pwr, double** omg) { - if (m <= 2048) return fill_reim_fft_bfs_16_omegas(m, entry_pwr, omg); - const uint64_t h = m / 2; - const double s = entry_pwr / 2; - (*omg)[0] = cos(2 * M_PI * s); - (*omg)[1] = sin(2 * M_PI * s); - *omg += 2; - fill_reim_fft_rec_16_omegas(h, s, omg); - fill_reim_fft_rec_16_omegas(h, s + 0.5, omg); -} - -void reim_fft_ref(const REIM_FFT_PRECOMP* precomp, double* dat) { - const int32_t m = precomp->m; - double* omg = precomp->powomegas; - double* re = dat; - double* im = dat+m; - if (m <= 16) { - switch (m) { - case 1: - return; - case 2: - return reim_fft2_ref(re, im, omg); - case 4: - return reim_fft4_ref(re, im, omg); - case 8: - return reim_fft8_ref(re, im, omg); - case 16: - return reim_fft16_ref(re, im, omg); - default: - abort(); // m is not a power of 2 - } - } - if (m <= 2048) return reim_fft_bfs_16_ref(m, re, im, &omg); - return reim_fft_rec_16_ref(m, re, im, &omg); -} - -EXPORT REIM_FFT_PRECOMP* new_reim_fft_precomp(uint32_t m, uint32_t num_buffers) { - const uint64_t OMG_SPACE = ceilto64b(2 * m * sizeof(double)); - const uint64_t BUF_SIZE = ceilto64b(2 * m * sizeof(double)); - void* reps = malloc(sizeof(REIM_FFT_PRECOMP) // base - + 63 // padding - + OMG_SPACE // tables //TODO 16? - + num_buffers * BUF_SIZE // buffers - ); - uint64_t aligned_addr = ceilto64b((uint64_t)(reps) + sizeof(REIM_FFT_PRECOMP)); - REIM_FFT_PRECOMP* r = (REIM_FFT_PRECOMP*)reps; - r->m = m; - r->buf_size = BUF_SIZE; - r->powomegas = (double*)aligned_addr; - r->aligned_buffers = (void*)(aligned_addr + OMG_SPACE); - // fill in powomegas - double* omg = (double*) r->powomegas; - if (m <= 16) { - switch (m) { - case 1: - break; - case 2: - fill_reim_fft2_omegas(0.25, &omg); - break; - case 4: - fill_reim_fft4_omegas(0.25, &omg); - break; - case 8: - fill_reim_fft8_omegas(0.25, &omg); - break; - case 16: - fill_reim_fft16_omegas(0.25, &omg); - break; - default: - abort(); // m is not a power of 2 - } - } else if (m <= 2048) { - fill_reim_fft_bfs_16_omegas(m, 0.25, &omg); - } else { - fill_reim_fft_rec_16_omegas(m, 0.25, &omg); - } - if (((uint64_t)omg) - aligned_addr > OMG_SPACE) abort(); - // dispatch the right implementation - { - if (CPU_SUPPORTS("fma")) { - r->function = reim_fft_avx2_fma; - } else { - r->function = reim_fft_ref; - } - } - return reps; -} - - -void reim_naive_fft(uint64_t m, double entry_pwr, double* re, double* im) { - if (m == 1) return; - // twiddle - const uint64_t h = m / 2; - const double s = entry_pwr / 2.; - const double sre = cos(2 * M_PI * s); - const double sim = sin(2 * M_PI * s); - for (uint64_t j = 0; j < h; ++j) { - double pre = re[h + j] * sre - im[h + j] * sim; - double pim = im[h + j] * sre + re[h + j] * sim; - re[h + j] = re[j] - pre; - im[h + j] = im[j] - pim; - re[j] += pre; - im[j] += pim; - } - reim_naive_fft(h, s, re, im); - reim_naive_fft(h, s + 0.5, re + h, im + h); -}
D spqlios/lib/spqlios/reim/reim_fftvec_addmul_fma.c

@@ -1,75 +0,0 @@

-#include <assert.h> -#include <immintrin.h> -#include <stdint.h> - -#include "reim_fft_private.h" - -EXPORT void reim_fftvec_addmul_fma(const REIM_FFTVEC_ADDMUL_PRECOMP* precomp, double* r, const double* a, - const double* b) { - const uint64_t m = precomp->m; - double* rr_ptr = r; - double* ri_ptr = r + m; - const double* ar_ptr = a; - const double* ai_ptr = a + m; - const double* br_ptr = b; - const double* bi_ptr = b + m; - - const double* const rend_ptr = ri_ptr; - while (rr_ptr != rend_ptr) { - __m256d rr = _mm256_loadu_pd(rr_ptr); - __m256d ri = _mm256_loadu_pd(ri_ptr); - const __m256d ar = _mm256_loadu_pd(ar_ptr); - const __m256d ai = _mm256_loadu_pd(ai_ptr); - const __m256d br = _mm256_loadu_pd(br_ptr); - const __m256d bi = _mm256_loadu_pd(bi_ptr); - - rr = _mm256_fmsub_pd(ai, bi, rr); - rr = _mm256_fmsub_pd(ar, br, rr); - ri = _mm256_fmadd_pd(ar, bi, ri); - ri = _mm256_fmadd_pd(ai, br, ri); - - _mm256_storeu_pd(rr_ptr, rr); - _mm256_storeu_pd(ri_ptr, ri); - - rr_ptr += 4; - ri_ptr += 4; - ar_ptr += 4; - ai_ptr += 4; - br_ptr += 4; - bi_ptr += 4; - } -} - -EXPORT void reim_fftvec_mul_fma(const REIM_FFTVEC_MUL_PRECOMP* precomp, double* r, const double* a, const double* b) { - const uint64_t m = precomp->m; - double* rr_ptr = r; - double* ri_ptr = r + m; - const double* ar_ptr = a; - const double* ai_ptr = a + m; - const double* br_ptr = b; - const double* bi_ptr = b + m; - - const double* const rend_ptr = ri_ptr; - while (rr_ptr != rend_ptr) { - const __m256d ar = _mm256_loadu_pd(ar_ptr); - const __m256d ai = _mm256_loadu_pd(ai_ptr); - const __m256d br = _mm256_loadu_pd(br_ptr); - const __m256d bi = _mm256_loadu_pd(bi_ptr); - - const __m256d t1 = _mm256_mul_pd(ai, bi); - const __m256d t2 = _mm256_mul_pd(ar, bi); - - __m256d rr = _mm256_fmsub_pd(ar, br, t1); - __m256d ri = _mm256_fmadd_pd(ai, br, t2); - - _mm256_storeu_pd(rr_ptr, rr); - _mm256_storeu_pd(ri_ptr, ri); - - rr_ptr += 4; - ri_ptr += 4; - ar_ptr += 4; - ai_ptr += 4; - br_ptr += 4; - bi_ptr += 4; - } -}
D spqlios/lib/spqlios/reim/reim_fftvec_addmul_ref.c

@@ -1,54 +0,0 @@

-#include "reim_fft_internal.h" -#include "reim_fft_private.h" - -EXPORT void reim_fftvec_addmul_ref(const REIM_FFTVEC_ADDMUL_PRECOMP* precomp, double* r, const double* a, - const double* b) { - const uint64_t m = precomp->m; - for (uint64_t i = 0; i < m; ++i) { - double re = a[i] * b[i] - a[i + m] * b[i + m]; - double im = a[i] * b[i + m] + a[i + m] * b[i]; - r[i] += re; - r[i + m] += im; - } -} - -EXPORT void reim_fftvec_mul_ref(const REIM_FFTVEC_MUL_PRECOMP* precomp, double* r, const double* a, const double* b) { - const uint64_t m = precomp->m; - for (uint64_t i = 0; i < m; ++i) { - double re = a[i] * b[i] - a[i + m] * b[i + m]; - double im = a[i] * b[i + m] + a[i + m] * b[i]; - r[i] = re; - r[i + m] = im; - } -} - -EXPORT REIM_FFTVEC_ADDMUL_PRECOMP* new_reim_fftvec_addmul_precomp(uint32_t m) { - REIM_FFTVEC_ADDMUL_PRECOMP* reps = malloc(sizeof(REIM_FFTVEC_ADDMUL_PRECOMP)); - reps->m = m; - if (CPU_SUPPORTS("fma")) { - if (m >= 4) { - reps->function = reim_fftvec_addmul_fma; - } else { - reps->function = reim_fftvec_addmul_ref; - } - } else { - reps->function = reim_fftvec_addmul_ref; - } - return reps; -} - -EXPORT REIM_FFTVEC_MUL_PRECOMP* new_reim_fftvec_mul_precomp(uint32_t m) { - REIM_FFTVEC_MUL_PRECOMP* reps = malloc(sizeof(REIM_FFTVEC_MUL_PRECOMP)); - reps->m = m; - if (CPU_SUPPORTS("fma")) { - if (m >= 4) { - reps->function = reim_fftvec_mul_fma; - } else { - reps->function = reim_fftvec_mul_ref; - } - } else { - reps->function = reim_fftvec_mul_ref; - } - return reps; -} -
D spqlios/lib/spqlios/reim/reim_ifft16_avx_fma.s

@@ -1,192 +0,0 @@

-#rdi datare ptr -#rsi dataim ptr -#rdx om ptr -.globl reim_ifft16_avx_fma -reim_ifft16_avx_fma: -vmovupd (%rdi),%ymm0 # ra0 -vmovupd 0x20(%rdi),%ymm1 # ra4 -vmovupd 0x40(%rdi),%ymm2 # ra8 -vmovupd 0x60(%rdi),%ymm3 # ra12 -vmovupd (%rsi),%ymm4 # ia0 -vmovupd 0x20(%rsi),%ymm5 # ia4 -vmovupd 0x40(%rsi),%ymm6 # ia8 -vmovupd 0x60(%rsi),%ymm7 # ia12 - -1: -vmovupd 0x00(%rdx),%ymm12 -vmovupd 0x20(%rdx),%ymm13 - -vperm2f128 $0x31,%ymm2,%ymm0,%ymm8 # ymm8 contains re to mul (tw) -vperm2f128 $0x31,%ymm3,%ymm1,%ymm9 # ymm9 contains re to mul (itw) -vperm2f128 $0x31,%ymm6,%ymm4,%ymm10 # ymm10 contains im to mul (tw) -vperm2f128 $0x31,%ymm7,%ymm5,%ymm11 # ymm11 contains im to mul (itw) -vperm2f128 $0x20,%ymm2,%ymm0,%ymm0 # ymm0 contains re to add (tw) -vperm2f128 $0x20,%ymm3,%ymm1,%ymm1 # ymm1 contains re to add (itw) -vperm2f128 $0x20,%ymm6,%ymm4,%ymm2 # ymm2 contains im to add (tw) -vperm2f128 $0x20,%ymm7,%ymm5,%ymm3 # ymm3 contains im to add (itw) - -vunpckhpd %ymm1,%ymm0,%ymm4 # (0,1) -> (0,4) -vunpckhpd %ymm3,%ymm2,%ymm6 # (2,3) -> (2,6) -vunpckhpd %ymm9,%ymm8,%ymm5 # (8,9) -> (1,5) -vunpckhpd %ymm11,%ymm10,%ymm7 # (10,11) -> (3,7) -vunpcklpd %ymm1,%ymm0,%ymm0 -vunpcklpd %ymm3,%ymm2,%ymm2 -vunpcklpd %ymm9,%ymm8,%ymm1 -vunpcklpd %ymm11,%ymm10,%ymm3 - -# invctwiddle Re:(ymm0,ymm4) and Im:(ymm2,ymm6) with omega=(ymm12,ymm13) -# invcitwiddle Re:(ymm1,ymm5) and Im:(ymm3,ymm7) with omega=(ymm12,ymm13) -vsubpd %ymm4,%ymm0,%ymm8 # retw -vsubpd %ymm5,%ymm1,%ymm9 # reitw -vsubpd %ymm6,%ymm2,%ymm10 # imtw -vsubpd %ymm7,%ymm3,%ymm11 # imitw -vaddpd %ymm4,%ymm0,%ymm0 -vaddpd %ymm5,%ymm1,%ymm1 -vaddpd %ymm6,%ymm2,%ymm2 -vaddpd %ymm7,%ymm3,%ymm3 -# multiply 8,9,10,11 by 12,13, result to: 4,5,6,7 -# twiddles use reom=ymm12, imom=ymm13 -# invtwiddles use reom=ymm13, imom=-ymm12 -vmulpd %ymm10,%ymm13,%ymm4 # imtw.omai (tw) -vmulpd %ymm11,%ymm12,%ymm5 # imitw.omar (itw) -vmulpd %ymm8,%ymm13,%ymm6 # retw.omai (tw) -vmulpd %ymm9,%ymm12,%ymm7 # reitw.omar (itw) -vfmsub231pd %ymm8,%ymm12,%ymm4 # rprod0 (tw) -vfmadd231pd %ymm9,%ymm13,%ymm5 # rprod4 (itw) -vfmadd231pd %ymm10,%ymm12,%ymm6 # iprod0 (tw) -vfmsub231pd %ymm11,%ymm13,%ymm7 # iprod4 (itw) - -vunpckhpd %ymm7,%ymm3,%ymm11 # (0,4) -> (0,1) -vunpckhpd %ymm5,%ymm1,%ymm9 # (2,6) -> (2,3) -vunpcklpd %ymm7,%ymm3,%ymm10 -vunpcklpd %ymm5,%ymm1,%ymm8 -vunpckhpd %ymm6,%ymm2,%ymm3 # (1,5) -> (8,9) -vunpckhpd %ymm4,%ymm0,%ymm1 # (3,7) -> (10,11) -vunpcklpd %ymm6,%ymm2,%ymm2 -vunpcklpd %ymm4,%ymm0,%ymm0 - -/* -vperm2f128 $0x31,%ymm10,%ymm2,%ymm6 -vperm2f128 $0x31,%ymm11,%ymm3,%ymm7 -vperm2f128 $0x20,%ymm10,%ymm2,%ymm4 -vperm2f128 $0x20,%ymm11,%ymm3,%ymm5 -vperm2f128 $0x31,%ymm8,%ymm0,%ymm2 -vperm2f128 $0x31,%ymm9,%ymm1,%ymm3 -vperm2f128 $0x20,%ymm8,%ymm0,%ymm0 -vperm2f128 $0x20,%ymm9,%ymm1,%ymm1 -*/ -2: -vmovupd 0x40(%rdx),%ymm12 -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omaiii'i' -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omarrr'r' - -/* -vperm2f128 $0x31,%ymm2,%ymm0,%ymm8 # ymm8 contains re to mul (tw) -vperm2f128 $0x31,%ymm3,%ymm1,%ymm9 # ymm9 contains re to mul (itw) -vperm2f128 $0x31,%ymm6,%ymm4,%ymm10 # ymm10 contains im to mul (tw) -vperm2f128 $0x31,%ymm7,%ymm5,%ymm11 # ymm11 contains im to mul (itw) -vperm2f128 $0x20,%ymm2,%ymm0,%ymm0 # ymm0 contains re to add (tw) -vperm2f128 $0x20,%ymm3,%ymm1,%ymm1 # ymm1 contains re to add (itw) -vperm2f128 $0x20,%ymm6,%ymm4,%ymm2 # ymm2 contains im to add (tw) -vperm2f128 $0x20,%ymm7,%ymm5,%ymm3 # ymm3 contains im to add (itw) -*/ - -# invctwiddle Re:(ymm0,ymm8) and Im:(ymm2,ymm10) with omega=(ymm12,ymm13) -# invcitwiddle Re:(ymm1,ymm9) and Im:(ymm3,ymm11) with omega=(ymm12,ymm13) -vsubpd %ymm8,%ymm0,%ymm4 # retw -vsubpd %ymm9,%ymm1,%ymm5 # reitw -vsubpd %ymm10,%ymm2,%ymm6 # imtw -vsubpd %ymm11,%ymm3,%ymm7 # imitw -vaddpd %ymm8,%ymm0,%ymm0 -vaddpd %ymm9,%ymm1,%ymm1 -vaddpd %ymm10,%ymm2,%ymm2 -vaddpd %ymm11,%ymm3,%ymm3 -# multiply 4,5,6,7 by 12,13, result to 8,9,10,11 -# twiddles use reom=ymm12, imom=ymm13 -# invtwiddles use reom=ymm13, imom=-ymm12 -vmulpd %ymm6,%ymm13,%ymm8 # imtw.omai (tw) -vmulpd %ymm7,%ymm12,%ymm9 # imitw.omar (itw) -vmulpd %ymm4,%ymm13,%ymm10 # retw.omai (tw) -vmulpd %ymm5,%ymm12,%ymm11 # reitw.omar (itw) -vfmsub231pd %ymm4,%ymm12,%ymm8 # rprod0 (tw) -vfmadd231pd %ymm5,%ymm13,%ymm9 # rprod4 (itw) -vfmadd231pd %ymm6,%ymm12,%ymm10 # iprod0 (tw) -vfmsub231pd %ymm7,%ymm13,%ymm11 # iprod4 (itw) - -vperm2f128 $0x31,%ymm10,%ymm2,%ymm6 -vperm2f128 $0x31,%ymm11,%ymm3,%ymm7 -vperm2f128 $0x20,%ymm10,%ymm2,%ymm4 -vperm2f128 $0x20,%ymm11,%ymm3,%ymm5 -vperm2f128 $0x31,%ymm8,%ymm0,%ymm2 -vperm2f128 $0x31,%ymm9,%ymm1,%ymm3 -vperm2f128 $0x20,%ymm8,%ymm0,%ymm0 -vperm2f128 $0x20,%ymm9,%ymm1,%ymm1 - -3: -vmovupd 0x60(%rdx),%xmm12 -vinsertf128 $1, %xmm12, %ymm12, %ymm12 # omriri -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omai -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omar - -# invctwiddle Re:(ymm0,ymm1) and Im:(ymm4,ymm5) with omega=(ymm12,ymm13) -# invcitwiddle Re:(ymm2,ymm3) and Im:(ymm6,ymm7) with omega=(ymm12,ymm13) -vsubpd %ymm1,%ymm0,%ymm8 # retw -vsubpd %ymm3,%ymm2,%ymm9 # reitw -vsubpd %ymm5,%ymm4,%ymm10 # imtw -vsubpd %ymm7,%ymm6,%ymm11 # imitw -vaddpd %ymm1,%ymm0,%ymm0 -vaddpd %ymm3,%ymm2,%ymm2 -vaddpd %ymm5,%ymm4,%ymm4 -vaddpd %ymm7,%ymm6,%ymm6 -# multiply 8,9,10,11 by 12,13, result to 1,3,5,7 -# twiddles use reom=ymm12, imom=ymm13 -# invtwiddles use reom=ymm13, imom=-ymm12 -vmulpd %ymm10,%ymm13,%ymm1 # imtw.omai (tw) -vmulpd %ymm11,%ymm12,%ymm3 # imitw.omar (itw) -vmulpd %ymm8,%ymm13,%ymm5 # retw.omai (tw) -vmulpd %ymm9,%ymm12,%ymm7 # reitw.omar (itw) -vfmsub231pd %ymm8,%ymm12,%ymm1 # rprod0 (tw) -vfmadd231pd %ymm9,%ymm13,%ymm3 # rprod4 (itw) -vfmadd231pd %ymm10,%ymm12,%ymm5 # iprod0 (tw) -vfmsub231pd %ymm11,%ymm13,%ymm7 # iprod4 (itw) - -4: -vmovupd 0x70(%rdx),%xmm12 -vinsertf128 $1, %xmm12, %ymm12, %ymm12 # omriri -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omai -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omar - -# invctwiddle Re:(ymm0,ymm2) and Im:(ymm4,ymm6) with omega=(ymm12,ymm13) -# invctwiddle Re:(ymm1,ymm3) and Im:(ymm5,ymm7) with omega=(ymm12,ymm13) -vsubpd %ymm2,%ymm0,%ymm8 # retw1 -vsubpd %ymm3,%ymm1,%ymm9 # retw2 -vsubpd %ymm6,%ymm4,%ymm10 # imtw1 -vsubpd %ymm7,%ymm5,%ymm11 # imtw2 -vaddpd %ymm2,%ymm0,%ymm0 -vaddpd %ymm3,%ymm1,%ymm1 -vaddpd %ymm6,%ymm4,%ymm4 -vaddpd %ymm7,%ymm5,%ymm5 -# multiply 8,9,10,11 by 12,13, result to 2,3,6,7 -# twiddles use reom=ymm12, imom=ymm13 -vmulpd %ymm10,%ymm13,%ymm2 # imtw1.omai -vmulpd %ymm11,%ymm13,%ymm3 # imtw2.omai -vmulpd %ymm8,%ymm13,%ymm6 # retw1.omai -vmulpd %ymm9,%ymm13,%ymm7 # retw2.omai -vfmsub231pd %ymm8,%ymm12,%ymm2 # rprod0 -vfmsub231pd %ymm9,%ymm12,%ymm3 # rprod4 -vfmadd231pd %ymm10,%ymm12,%ymm6 # iprod0 -vfmadd231pd %ymm11,%ymm12,%ymm7 # iprod4 - -5: -vmovupd %ymm0,(%rdi) # ra0 -vmovupd %ymm1,0x20(%rdi) # ra4 -vmovupd %ymm2,0x40(%rdi) # ra8 -vmovupd %ymm3,0x60(%rdi) # ra12 -vmovupd %ymm4,(%rsi) # ia0 -vmovupd %ymm5,0x20(%rsi) # ia4 -vmovupd %ymm6,0x40(%rsi) # ia8 -vmovupd %ymm7,0x60(%rsi) # ia12 -vzeroupper -ret -.size reim_ifft16_avx_fma, .-reim_ifft16_avx_fma -.section .note.GNU-stack,"",@progbits
D spqlios/lib/spqlios/reim/reim_ifft16_avx_fma_win32.s

@@ -1,228 +0,0 @@

- .text - .p2align 4 - .globl reim_ifft16_avx_fma - .def reim_ifft16_avx_fma; .scl 2; .type 32; .endef -reim_ifft16_avx_fma: - - pushq %rdi - pushq %rsi - movq %rcx,%rdi - movq %rdx,%rsi - movq %r8,%rdx - subq $0x100,%rsp - movdqu %xmm6,(%rsp) - movdqu %xmm7,0x10(%rsp) - movdqu %xmm8,0x20(%rsp) - movdqu %xmm9,0x30(%rsp) - movdqu %xmm10,0x40(%rsp) - movdqu %xmm11,0x50(%rsp) - movdqu %xmm12,0x60(%rsp) - movdqu %xmm13,0x70(%rsp) - movdqu %xmm14,0x80(%rsp) - movdqu %xmm15,0x90(%rsp) - callq reim_ifft16_avx_fma_amd64 - movdqu (%rsp),%xmm6 - movdqu 0x10(%rsp),%xmm7 - movdqu 0x20(%rsp),%xmm8 - movdqu 0x30(%rsp),%xmm9 - movdqu 0x40(%rsp),%xmm10 - movdqu 0x50(%rsp),%xmm11 - movdqu 0x60(%rsp),%xmm12 - movdqu 0x70(%rsp),%xmm13 - movdqu 0x80(%rsp),%xmm14 - movdqu 0x90(%rsp),%xmm15 - addq $0x100,%rsp - popq %rsi - popq %rdi - retq - -#rdi datare ptr -#rsi dataim ptr -#rdx om ptr -#.globl reim_ifft16_avx_fma_amd64 -reim_ifft16_avx_fma_amd64: -vmovupd (%rdi),%ymm0 # ra0 -vmovupd 0x20(%rdi),%ymm1 # ra4 -vmovupd 0x40(%rdi),%ymm2 # ra8 -vmovupd 0x60(%rdi),%ymm3 # ra12 -vmovupd (%rsi),%ymm4 # ia0 -vmovupd 0x20(%rsi),%ymm5 # ia4 -vmovupd 0x40(%rsi),%ymm6 # ia8 -vmovupd 0x60(%rsi),%ymm7 # ia12 - -1: -vmovupd 0x00(%rdx),%ymm12 -vmovupd 0x20(%rdx),%ymm13 - -vperm2f128 $0x31,%ymm2,%ymm0,%ymm8 # ymm8 contains re to mul (tw) -vperm2f128 $0x31,%ymm3,%ymm1,%ymm9 # ymm9 contains re to mul (itw) -vperm2f128 $0x31,%ymm6,%ymm4,%ymm10 # ymm10 contains im to mul (tw) -vperm2f128 $0x31,%ymm7,%ymm5,%ymm11 # ymm11 contains im to mul (itw) -vperm2f128 $0x20,%ymm2,%ymm0,%ymm0 # ymm0 contains re to add (tw) -vperm2f128 $0x20,%ymm3,%ymm1,%ymm1 # ymm1 contains re to add (itw) -vperm2f128 $0x20,%ymm6,%ymm4,%ymm2 # ymm2 contains im to add (tw) -vperm2f128 $0x20,%ymm7,%ymm5,%ymm3 # ymm3 contains im to add (itw) - -vunpckhpd %ymm1,%ymm0,%ymm4 # (0,1) -> (0,4) -vunpckhpd %ymm3,%ymm2,%ymm6 # (2,3) -> (2,6) -vunpckhpd %ymm9,%ymm8,%ymm5 # (8,9) -> (1,5) -vunpckhpd %ymm11,%ymm10,%ymm7 # (10,11) -> (3,7) -vunpcklpd %ymm1,%ymm0,%ymm0 -vunpcklpd %ymm3,%ymm2,%ymm2 -vunpcklpd %ymm9,%ymm8,%ymm1 -vunpcklpd %ymm11,%ymm10,%ymm3 - -# invctwiddle Re:(ymm0,ymm4) and Im:(ymm2,ymm6) with omega=(ymm12,ymm13) -# invcitwiddle Re:(ymm1,ymm5) and Im:(ymm3,ymm7) with omega=(ymm12,ymm13) -vsubpd %ymm4,%ymm0,%ymm8 # retw -vsubpd %ymm5,%ymm1,%ymm9 # reitw -vsubpd %ymm6,%ymm2,%ymm10 # imtw -vsubpd %ymm7,%ymm3,%ymm11 # imitw -vaddpd %ymm4,%ymm0,%ymm0 -vaddpd %ymm5,%ymm1,%ymm1 -vaddpd %ymm6,%ymm2,%ymm2 -vaddpd %ymm7,%ymm3,%ymm3 -# multiply 8,9,10,11 by 12,13, result to: 4,5,6,7 -# twiddles use reom=ymm12, imom=ymm13 -# invtwiddles use reom=ymm13, imom=-ymm12 -vmulpd %ymm10,%ymm13,%ymm4 # imtw.omai (tw) -vmulpd %ymm11,%ymm12,%ymm5 # imitw.omar (itw) -vmulpd %ymm8,%ymm13,%ymm6 # retw.omai (tw) -vmulpd %ymm9,%ymm12,%ymm7 # reitw.omar (itw) -vfmsub231pd %ymm8,%ymm12,%ymm4 # rprod0 (tw) -vfmadd231pd %ymm9,%ymm13,%ymm5 # rprod4 (itw) -vfmadd231pd %ymm10,%ymm12,%ymm6 # iprod0 (tw) -vfmsub231pd %ymm11,%ymm13,%ymm7 # iprod4 (itw) - -vunpckhpd %ymm7,%ymm3,%ymm11 # (0,4) -> (0,1) -vunpckhpd %ymm5,%ymm1,%ymm9 # (2,6) -> (2,3) -vunpcklpd %ymm7,%ymm3,%ymm10 -vunpcklpd %ymm5,%ymm1,%ymm8 -vunpckhpd %ymm6,%ymm2,%ymm3 # (1,5) -> (8,9) -vunpckhpd %ymm4,%ymm0,%ymm1 # (3,7) -> (10,11) -vunpcklpd %ymm6,%ymm2,%ymm2 -vunpcklpd %ymm4,%ymm0,%ymm0 - -/* -vperm2f128 $0x31,%ymm10,%ymm2,%ymm6 -vperm2f128 $0x31,%ymm11,%ymm3,%ymm7 -vperm2f128 $0x20,%ymm10,%ymm2,%ymm4 -vperm2f128 $0x20,%ymm11,%ymm3,%ymm5 -vperm2f128 $0x31,%ymm8,%ymm0,%ymm2 -vperm2f128 $0x31,%ymm9,%ymm1,%ymm3 -vperm2f128 $0x20,%ymm8,%ymm0,%ymm0 -vperm2f128 $0x20,%ymm9,%ymm1,%ymm1 -*/ -2: -vmovupd 0x40(%rdx),%ymm12 -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omaiii'i' -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omarrr'r' - -/* -vperm2f128 $0x31,%ymm2,%ymm0,%ymm8 # ymm8 contains re to mul (tw) -vperm2f128 $0x31,%ymm3,%ymm1,%ymm9 # ymm9 contains re to mul (itw) -vperm2f128 $0x31,%ymm6,%ymm4,%ymm10 # ymm10 contains im to mul (tw) -vperm2f128 $0x31,%ymm7,%ymm5,%ymm11 # ymm11 contains im to mul (itw) -vperm2f128 $0x20,%ymm2,%ymm0,%ymm0 # ymm0 contains re to add (tw) -vperm2f128 $0x20,%ymm3,%ymm1,%ymm1 # ymm1 contains re to add (itw) -vperm2f128 $0x20,%ymm6,%ymm4,%ymm2 # ymm2 contains im to add (tw) -vperm2f128 $0x20,%ymm7,%ymm5,%ymm3 # ymm3 contains im to add (itw) -*/ - -# invctwiddle Re:(ymm0,ymm8) and Im:(ymm2,ymm10) with omega=(ymm12,ymm13) -# invcitwiddle Re:(ymm1,ymm9) and Im:(ymm3,ymm11) with omega=(ymm12,ymm13) -vsubpd %ymm8,%ymm0,%ymm4 # retw -vsubpd %ymm9,%ymm1,%ymm5 # reitw -vsubpd %ymm10,%ymm2,%ymm6 # imtw -vsubpd %ymm11,%ymm3,%ymm7 # imitw -vaddpd %ymm8,%ymm0,%ymm0 -vaddpd %ymm9,%ymm1,%ymm1 -vaddpd %ymm10,%ymm2,%ymm2 -vaddpd %ymm11,%ymm3,%ymm3 -# multiply 4,5,6,7 by 12,13, result to 8,9,10,11 -# twiddles use reom=ymm12, imom=ymm13 -# invtwiddles use reom=ymm13, imom=-ymm12 -vmulpd %ymm6,%ymm13,%ymm8 # imtw.omai (tw) -vmulpd %ymm7,%ymm12,%ymm9 # imitw.omar (itw) -vmulpd %ymm4,%ymm13,%ymm10 # retw.omai (tw) -vmulpd %ymm5,%ymm12,%ymm11 # reitw.omar (itw) -vfmsub231pd %ymm4,%ymm12,%ymm8 # rprod0 (tw) -vfmadd231pd %ymm5,%ymm13,%ymm9 # rprod4 (itw) -vfmadd231pd %ymm6,%ymm12,%ymm10 # iprod0 (tw) -vfmsub231pd %ymm7,%ymm13,%ymm11 # iprod4 (itw) - -vperm2f128 $0x31,%ymm10,%ymm2,%ymm6 -vperm2f128 $0x31,%ymm11,%ymm3,%ymm7 -vperm2f128 $0x20,%ymm10,%ymm2,%ymm4 -vperm2f128 $0x20,%ymm11,%ymm3,%ymm5 -vperm2f128 $0x31,%ymm8,%ymm0,%ymm2 -vperm2f128 $0x31,%ymm9,%ymm1,%ymm3 -vperm2f128 $0x20,%ymm8,%ymm0,%ymm0 -vperm2f128 $0x20,%ymm9,%ymm1,%ymm1 - -3: -vmovupd 0x60(%rdx),%xmm12 -vinsertf128 $1, %xmm12, %ymm12, %ymm12 # omriri -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omai -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omar - -# invctwiddle Re:(ymm0,ymm1) and Im:(ymm4,ymm5) with omega=(ymm12,ymm13) -# invcitwiddle Re:(ymm2,ymm3) and Im:(ymm6,ymm7) with omega=(ymm12,ymm13) -vsubpd %ymm1,%ymm0,%ymm8 # retw -vsubpd %ymm3,%ymm2,%ymm9 # reitw -vsubpd %ymm5,%ymm4,%ymm10 # imtw -vsubpd %ymm7,%ymm6,%ymm11 # imitw -vaddpd %ymm1,%ymm0,%ymm0 -vaddpd %ymm3,%ymm2,%ymm2 -vaddpd %ymm5,%ymm4,%ymm4 -vaddpd %ymm7,%ymm6,%ymm6 -# multiply 8,9,10,11 by 12,13, result to 1,3,5,7 -# twiddles use reom=ymm12, imom=ymm13 -# invtwiddles use reom=ymm13, imom=-ymm12 -vmulpd %ymm10,%ymm13,%ymm1 # imtw.omai (tw) -vmulpd %ymm11,%ymm12,%ymm3 # imitw.omar (itw) -vmulpd %ymm8,%ymm13,%ymm5 # retw.omai (tw) -vmulpd %ymm9,%ymm12,%ymm7 # reitw.omar (itw) -vfmsub231pd %ymm8,%ymm12,%ymm1 # rprod0 (tw) -vfmadd231pd %ymm9,%ymm13,%ymm3 # rprod4 (itw) -vfmadd231pd %ymm10,%ymm12,%ymm5 # iprod0 (tw) -vfmsub231pd %ymm11,%ymm13,%ymm7 # iprod4 (itw) - -4: -vmovupd 0x70(%rdx),%xmm12 -vinsertf128 $1, %xmm12, %ymm12, %ymm12 # omriri -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omai -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omar - -# invctwiddle Re:(ymm0,ymm2) and Im:(ymm4,ymm6) with omega=(ymm12,ymm13) -# invctwiddle Re:(ymm1,ymm3) and Im:(ymm5,ymm7) with omega=(ymm12,ymm13) -vsubpd %ymm2,%ymm0,%ymm8 # retw1 -vsubpd %ymm3,%ymm1,%ymm9 # retw2 -vsubpd %ymm6,%ymm4,%ymm10 # imtw1 -vsubpd %ymm7,%ymm5,%ymm11 # imtw2 -vaddpd %ymm2,%ymm0,%ymm0 -vaddpd %ymm3,%ymm1,%ymm1 -vaddpd %ymm6,%ymm4,%ymm4 -vaddpd %ymm7,%ymm5,%ymm5 -# multiply 8,9,10,11 by 12,13, result to 2,3,6,7 -# twiddles use reom=ymm12, imom=ymm13 -vmulpd %ymm10,%ymm13,%ymm2 # imtw1.omai -vmulpd %ymm11,%ymm13,%ymm3 # imtw2.omai -vmulpd %ymm8,%ymm13,%ymm6 # retw1.omai -vmulpd %ymm9,%ymm13,%ymm7 # retw2.omai -vfmsub231pd %ymm8,%ymm12,%ymm2 # rprod0 -vfmsub231pd %ymm9,%ymm12,%ymm3 # rprod4 -vfmadd231pd %ymm10,%ymm12,%ymm6 # iprod0 -vfmadd231pd %ymm11,%ymm12,%ymm7 # iprod4 - -5: -vmovupd %ymm0,(%rdi) # ra0 -vmovupd %ymm1,0x20(%rdi) # ra4 -vmovupd %ymm2,0x40(%rdi) # ra8 -vmovupd %ymm3,0x60(%rdi) # ra12 -vmovupd %ymm4,(%rsi) # ia0 -vmovupd %ymm5,0x20(%rsi) # ia4 -vmovupd %ymm6,0x40(%rsi) # ia8 -vmovupd %ymm7,0x60(%rsi) # ia12 -vzeroupper -ret
D spqlios/lib/spqlios/reim/reim_ifft4_avx_fma.c

@@ -1,62 +0,0 @@

-#include <assert.h> -#include <immintrin.h> -#include <stdint.h> - -#include "reim_fft_private.h" - -__always_inline void reim_invctwiddle_avx_fma(__m128d* ra, __m128d* rb, __m128d* ia, __m128d* ib, const __m128d omre, - const __m128d omim) { - __m128d rdiff = _mm_sub_pd(*ra, *rb); - __m128d idiff = _mm_sub_pd(*ia, *ib); - *ra = _mm_add_pd(*ra, *rb); - *ia = _mm_add_pd(*ia, *ib); - - *rb = _mm_mul_pd(idiff, omim); - *rb = _mm_fmsub_pd(rdiff, omre, *rb); - - *ib = _mm_mul_pd(rdiff, omim); - *ib = _mm_fmadd_pd(idiff, omre, *ib); -} - -EXPORT void reim_ifft4_avx_fma(double* dre, double* dim, const void* ompv) { - const double* omp = (const double*)ompv; - - __m128d ra0 = _mm_loadu_pd(dre); - __m128d ra2 = _mm_loadu_pd(dre + 2); - __m128d ia0 = _mm_loadu_pd(dim); - __m128d ia2 = _mm_loadu_pd(dim + 2); - - // 1 - { - const __m128d ifft4neg = _mm_castsi128_pd(_mm_set_epi64x(UINT64_C(1) << 63, 0)); - __m128d omre = _mm_loadu_pd(omp); // omre: r,i - __m128d omim = _mm_xor_pd(_mm_permute_pd(omre, 1), ifft4neg); // omim: i,-r - - __m128d ra = _mm_unpacklo_pd(ra0, ra2); // (r0, r1), (r2, r3) -> (r0, r2) - __m128d ia = _mm_unpacklo_pd(ia0, ia2); // (i0, i1), (i2, i3) -> (i0, i2) - __m128d rb = _mm_unpackhi_pd(ra0, ra2); // (r0, r1), (r2, r3) -> (r1, r3) - __m128d ib = _mm_unpackhi_pd(ia0, ia2); // (i0, i1), (i2, i3) -> (i1, i3) - - reim_invctwiddle_avx_fma(&ra, &rb, &ia, &ib, omre, omim); - - ra0 = _mm_unpacklo_pd(ra, rb); - ia0 = _mm_unpacklo_pd(ia, ib); - ra2 = _mm_unpackhi_pd(ra, rb); - ia2 = _mm_unpackhi_pd(ia, ib); - } - - // 2 - { - __m128d om = _mm_loadu_pd(omp + 2); - __m128d omre = _mm_permute_pd(om, 0); - __m128d omim = _mm_permute_pd(om, 3); - - reim_invctwiddle_avx_fma(&ra0, &ra2, &ia0, &ia2, omre, omim); - } - - // 4 - _mm_storeu_pd(dre, ra0); - _mm_storeu_pd(dre + 2, ra2); - _mm_storeu_pd(dim, ia0); - _mm_storeu_pd(dim + 2, ia2); -}
D spqlios/lib/spqlios/reim/reim_ifft8_avx_fma.c

@@ -1,86 +0,0 @@

-#include <assert.h> -#include <immintrin.h> -#include <stdint.h> - -#include "reim_fft_private.h" - -__always_inline void reim_invctwiddle_avx_fma(__m256d* ra, __m256d* rb, __m256d* ia, __m256d* ib, const __m256d omre, - const __m256d omim) { - __m256d rdiff = _mm256_sub_pd(*ra, *rb); - __m256d idiff = _mm256_sub_pd(*ia, *ib); - *ra = _mm256_add_pd(*ra, *rb); - *ia = _mm256_add_pd(*ia, *ib); - - *rb = _mm256_mul_pd(idiff, omim); - *rb = _mm256_fmsub_pd(rdiff, omre, *rb); - - *ib = _mm256_mul_pd(rdiff, omim); - *ib = _mm256_fmadd_pd(idiff, omre, *ib); -} - -EXPORT void reim_ifft8_avx_fma(double* dre, double* dim, const void* ompv) { - const double* omp = (const double*)ompv; - - __m256d ra0 = _mm256_loadu_pd(dre); - __m256d ra4 = _mm256_loadu_pd(dre + 4); - __m256d ia0 = _mm256_loadu_pd(dim); - __m256d ia4 = _mm256_loadu_pd(dim + 4); - - // 1 - { - const __m256d fft8neg2 = _mm256_castsi256_pd(_mm256_set_epi64x(UINT64_C(1) << 63, UINT64_C(1) << 63, 0, 0)); - __m256d omr = _mm256_loadu_pd(omp); // r0,r1,i0,i1 - __m256d omiirr = _mm256_permute2f128_pd(omr, omr, 1); // i0,i1,r0,r1 - __m256d omi = _mm256_xor_pd(omiirr, fft8neg2); // i0,i1,-r0,-r1 - - __m256d rb = _mm256_unpackhi_pd(ra0, ra4); - __m256d ib = _mm256_unpackhi_pd(ia0, ia4); - __m256d ra = _mm256_unpacklo_pd(ra0, ra4); - __m256d ia = _mm256_unpacklo_pd(ia0, ia4); - - reim_invctwiddle_avx_fma(&ra, &rb, &ia, &ib, omr, omi); - - ra4 = _mm256_unpackhi_pd(ra, rb); - ia4 = _mm256_unpackhi_pd(ia, ib); - ra0 = _mm256_unpacklo_pd(ra, rb); - ia0 = _mm256_unpacklo_pd(ia, ib); - } - - // 2 - { - const __m128d ifft8neg = _mm_castsi128_pd(_mm_set_epi64x(0, UINT64_C(1) << 63)); - __m128d omri = _mm_loadu_pd(omp + 4); // r,i - __m128d ommri = _mm_xor_pd(omri, ifft8neg); // -r,i - __m256d omrimri = _mm256_set_m128d(ommri, omri); // r,i,-r,i - __m256d omi = _mm256_permute_pd(omrimri, 3); // i,i,-r,-r - __m256d omr = _mm256_permute_pd(omrimri, 12); // r,r,i,i - - __m256d rb = _mm256_permute2f128_pd(ra0, ra4, 0x31); - __m256d ib = _mm256_permute2f128_pd(ia0, ia4, 0x31); - __m256d ra = _mm256_permute2f128_pd(ra0, ra4, 0x20); - __m256d ia = _mm256_permute2f128_pd(ia0, ia4, 0x20); - - reim_invctwiddle_avx_fma(&ra, &rb, &ia, &ib, omr, omi); - - ra0 = _mm256_permute2f128_pd(ra, rb, 0x20); - ra4 = _mm256_permute2f128_pd(ra, rb, 0x31); - ia0 = _mm256_permute2f128_pd(ia, ib, 0x20); - ia4 = _mm256_permute2f128_pd(ia, ib, 0x31); - } - - // 3 - { - __m128d omri = _mm_loadu_pd(omp + 6); // r,i - __m256d omriri = _mm256_set_m128d(omri, omri); // r,i,r,i - __m256d omi = _mm256_permute_pd(omriri, 15); // i,i,i,i - __m256d omr = _mm256_permute_pd(omriri, 0); // r,r,r,r - - reim_invctwiddle_avx_fma(&ra0, &ra4, &ia0, &ia4, omr, omi); - } - - // 4 - _mm256_storeu_pd(dre, ra0); - _mm256_storeu_pd(dre + 4, ra4); - _mm256_storeu_pd(dim, ia0); - _mm256_storeu_pd(dim + 4, ia4); -}
D spqlios/lib/spqlios/reim/reim_ifft_avx2.c

@@ -1,167 +0,0 @@

-#include "immintrin.h" -#include "reim_fft_internal.h" -#include "reim_fft_private.h" - -void reim_invtwiddle_ifft_ref(uint64_t h, double* re, double* im, double om[2]); -void reim_invbitwiddle_ifft_ref(uint64_t h, double* re, double* im, double om[4]); - -__always_inline void reim_invtwiddle_ifft_avx2_fma(uint32_t h, double* re, double* im, double om[2]) { - const __m128d omx = _mm_load_pd(om); - const __m256d omra = _mm256_set_m128d(omx, omx); - const __m256d omi = _mm256_unpackhi_pd(omra, omra); - const __m256d omr = _mm256_unpacklo_pd(omra, omra); - double* r0 = re; - double* r1 = re + h; - double* i0 = im; - double* i1 = im + h; - for (uint32_t i=0; i<h; i+=4) { - __m256d ur0 = _mm256_loadu_pd(r0+i); - __m256d ur1 = _mm256_loadu_pd(r1+i); - __m256d ui0 = _mm256_loadu_pd(i0+i); - __m256d ui1 = _mm256_loadu_pd(i1+i); - __m256d tra = _mm256_sub_pd(ur0, ur1); - __m256d tia = _mm256_sub_pd(ui0, ui1); - ur0 = _mm256_add_pd(ur0, ur1); - ui0 = _mm256_add_pd(ui0, ui1); - ur1 = _mm256_mul_pd(omi, tia); - ui1 = _mm256_mul_pd(omi, tra); - ur1 = _mm256_fmsub_pd(omr, tra, ur1); - ui1 = _mm256_fmadd_pd(omr, tia, ui1); - _mm256_storeu_pd(r0+i, ur0); - _mm256_storeu_pd(r1+i, ur1); - _mm256_storeu_pd(i0+i, ui0); - _mm256_storeu_pd(i1+i, ui1); - } -} - -__always_inline void reim_invbitwiddle_ifft_avx2_fma(uint32_t h, double* re, double* im, double om[4]) { - //reim_invbitwiddle_ifft_ref(h,re,im, om); - double* const r0 = re; - double* const r1 = re + h; - double* const r2 = re + 2*h; - double* const r3 = re + 3*h; - double* const i0 = im; - double* const i1 = im + h; - double* const i2 = im + 2*h; - double* const i3 = im + 3*h; - const __m256d om0 = _mm256_loadu_pd(om); - const __m256d omb = _mm256_permute2f128_pd(om0,om0,0x11); - const __m256d oma = _mm256_permute2f128_pd(om0,om0,0x00); - const __m256d omai = _mm256_unpackhi_pd(oma, oma); - const __m256d omar = _mm256_unpacklo_pd(oma, oma); - const __m256d ombi = _mm256_unpackhi_pd(omb, omb); - const __m256d ombr = _mm256_unpacklo_pd(omb, omb); - for (uint32_t i=0; i<h; i+=4) { - __m256d ur0 = _mm256_loadu_pd(r0+i); - __m256d ur1 = _mm256_loadu_pd(r1+i); - __m256d ur2 = _mm256_loadu_pd(r2+i); - __m256d ur3 = _mm256_loadu_pd(r3+i); - __m256d ui0 = _mm256_loadu_pd(i0+i); - __m256d ui1 = _mm256_loadu_pd(i1+i); - __m256d ui2 = _mm256_loadu_pd(i2+i); - __m256d ui3 = _mm256_loadu_pd(i3+i); - __m256d tra,trb,tia,tib; - //------ twiddles 2 - tra = _mm256_sub_pd(ur0, ur1); - trb = _mm256_sub_pd(ur2, ur3); - tia = _mm256_sub_pd(ui0, ui1); - tib = _mm256_sub_pd(ui2, ui3); - ur0 = _mm256_add_pd(ur0, ur1); - ur2 = _mm256_add_pd(ur2, ur3); - ui0 = _mm256_add_pd(ui0, ui1); - ui2 = _mm256_add_pd(ui2, ui3); - ur1 = _mm256_mul_pd(omai, tia); - ur3 = _mm256_mul_pd(omar, tib); - ui1 = _mm256_mul_pd(omai, tra); - ui3 = _mm256_mul_pd(omar, trb); - ur1 = _mm256_fmsub_pd(omar, tra, ur1); - ur3 = _mm256_fmadd_pd(omai, trb, ur3); - ui1 = _mm256_fmadd_pd(omar, tia, ui1); - ui3 = _mm256_fmsub_pd(omai, tib, ui3); - //------ twiddles 1 - tra = _mm256_sub_pd(ur0, ur2); - trb = _mm256_sub_pd(ur1, ur3); - tia = _mm256_sub_pd(ui0, ui2); - tib = _mm256_sub_pd(ui1, ui3); - ur0 = _mm256_add_pd(ur0, ur2); - ur1 = _mm256_add_pd(ur1, ur3); - ui0 = _mm256_add_pd(ui0, ui2); - ui1 = _mm256_add_pd(ui1, ui3); - ur2 = _mm256_mul_pd(ombi, tia); - ur3 = _mm256_mul_pd(ombi, tib); - ui2 = _mm256_mul_pd(ombi, tra); - ui3 = _mm256_mul_pd(ombi, trb); - ur2 = _mm256_fmsub_pd(ombr, tra, ur2); - ur3 = _mm256_fmsub_pd(ombr, trb, ur3); - ui2 = _mm256_fmadd_pd(ombr, tia, ui2); - ui3 = _mm256_fmadd_pd(ombr, tib, ui3); - ///--- - _mm256_storeu_pd(r0+i, ur0); - _mm256_storeu_pd(r1+i, ur1); - _mm256_storeu_pd(r2+i, ur2); - _mm256_storeu_pd(r3+i, ur3); - _mm256_storeu_pd(i0+i, ui0); - _mm256_storeu_pd(i1+i, ui1); - _mm256_storeu_pd(i2+i, ui2); - _mm256_storeu_pd(i3+i, ui3); - } -} - -void reim_ifft_bfs_16_avx2_fma(uint32_t m, double* re, double* im, double** omg) { - uint32_t log2m = _mm_popcnt_u32(m-1); //log2(m); - for (uint32_t off = 0; off < m; off += 16) { - reim_ifft16_avx_fma(re+off, im+off, *omg); - *omg += 16; - } - uint32_t h = 16; - uint32_t ms2 = m>>1; // m/2 - while (h < ms2) { - uint32_t mm = h << 2; - for (uint32_t off = 0; off < m; off += mm) { - reim_invbitwiddle_ifft_avx2_fma(h, re + off, im + off, *omg); - *omg += 4; - } - h = mm; - } - if (log2m & 1) { - //if (h!=ms2) abort(); // bug - // do the first twiddle iteration normally - reim_invtwiddle_ifft_avx2_fma(h, re, im, *omg); - *omg += 2; - //h = m; - } -} - -void reim_ifft_rec_16_avx2_fma(uint32_t m, double* re, double* im, double** omg) { - if (m <= 2048) return reim_ifft_bfs_16_avx2_fma(m, re, im, omg); - const uint32_t h = m >> 1; //m / 2; - reim_ifft_rec_16_avx2_fma(h, re, im, omg); - reim_ifft_rec_16_avx2_fma(h, re + h, im + h, omg); - reim_invtwiddle_ifft_avx2_fma(h, re, im, *omg); - *omg += 2; -} - -void reim_ifft_avx2_fma(const REIM_IFFT_PRECOMP* precomp, double* dat) { - const int32_t m = precomp->m; - double* omg = precomp->powomegas; - double* re = dat; - double* im = dat+m; - if (m <= 16) { - switch (m) { - case 1: - return; - case 2: - return reim_ifft2_ref(re, im, omg); - case 4: - return reim_ifft4_avx_fma(re, im, omg); - case 8: - return reim_ifft8_avx_fma(re, im, omg); - case 16: - return reim_ifft16_avx_fma(re, im, omg); - default: - abort(); // m is not a power of 2 - } - } - if (m <= 2048) return reim_ifft_bfs_16_avx2_fma(m, re, im, &omg); - return reim_ifft_rec_16_avx2_fma(m, re, im, &omg); -}
D spqlios/lib/spqlios/reim/reim_ifft_ref.c

@@ -1,409 +0,0 @@

-#include "../commons_private.h" -#include "reim_fft_internal.h" -#include "reim_fft_private.h" - -void reim_invctwiddle(double* ra, double* ia, double* rb, double* ib, double omre, double omim) { - double rdiff = *ra - *rb; - double idiff = *ia - *ib; - *ra = *ra + *rb; - *ia = *ia + *ib; - *rb = rdiff * omre - idiff * omim; - *ib = rdiff * omim + idiff * omre; -} - -// i (omre + i omim) = -omim + i omre -void reim_invcitwiddle(double* ra, double* ia, double* rb, double* ib, double omre, double omim) { - double rdiff = *ra - *rb; - double idiff = *ia - *ib; - *ra = *ra + *rb; - *ia = *ia + *ib; - *rb = rdiff * omim + idiff * omre; - *ib = - rdiff * omre + idiff * omim; -} - -void reim_ifft16_ref(double* dre, double* dim, const void* pom) { - const double* om = (const double*)pom; - { - double omare = om[0]; - double ombre = om[1]; - double omcre = om[2]; - double omdre = om[3]; - double omaim = om[4]; - double ombim = om[5]; - double omcim = om[6]; - double omdim = om[7]; - reim_invctwiddle(&dre[0], &dim[0], &dre[1], &dim[1], omare, omaim); - reim_invcitwiddle(&dre[2], &dim[2], &dre[3], &dim[3], omare, omaim); - reim_invctwiddle(&dre[4], &dim[4], &dre[5], &dim[5], ombre, ombim); - reim_invcitwiddle(&dre[6], &dim[6], &dre[7], &dim[7], ombre, ombim); - reim_invctwiddle(&dre[8], &dim[8], &dre[9], &dim[9], omcre, omcim); - reim_invcitwiddle(&dre[10], &dim[10], &dre[11], &dim[11], omcre, omcim); - reim_invctwiddle(&dre[12], &dim[12], &dre[13], &dim[13], omdre, omdim); - reim_invcitwiddle(&dre[14], &dim[14], &dre[15], &dim[15], omdre, omdim); - } - { - double omare = om[8]; - double omaim = om[9]; - double ombre = om[10]; - double ombim = om[11]; - reim_invctwiddle(&dre[0], &dim[0], &dre[2], &dim[2], omare, omaim); - reim_invctwiddle(&dre[1], &dim[1], &dre[3], &dim[3], omare, omaim); - reim_invcitwiddle(&dre[4], &dim[4], &dre[6], &dim[6], omare, omaim); - reim_invcitwiddle(&dre[5], &dim[5], &dre[7], &dim[7], omare, omaim); - reim_invctwiddle(&dre[8], &dim[8], &dre[10], &dim[10], ombre, ombim); - reim_invctwiddle(&dre[9], &dim[9], &dre[11], &dim[11], ombre, ombim); - reim_invcitwiddle(&dre[12], &dim[12], &dre[14], &dim[14], ombre, ombim); - reim_invcitwiddle(&dre[13], &dim[13], &dre[15], &dim[15], ombre, ombim); - } - { - double omre = om[12]; - double omim = om[13]; - reim_invctwiddle(&dre[0], &dim[0], &dre[4], &dim[4], omre, omim); - reim_invctwiddle(&dre[1], &dim[1], &dre[5], &dim[5], omre, omim); - reim_invctwiddle(&dre[2], &dim[2], &dre[6], &dim[6], omre, omim); - reim_invctwiddle(&dre[3], &dim[3], &dre[7], &dim[7], omre, omim); - reim_invcitwiddle(&dre[8], &dim[8], &dre[12], &dim[12], omre, omim); - reim_invcitwiddle(&dre[9], &dim[9], &dre[13], &dim[13], omre, omim); - reim_invcitwiddle(&dre[10], &dim[10], &dre[14], &dim[14], omre, omim); - reim_invcitwiddle(&dre[11], &dim[11], &dre[15], &dim[15], omre, omim); - } - { - double omre = om[14]; - double omim = om[15]; - reim_invctwiddle(&dre[0], &dim[0], &dre[8], &dim[8], omre, omim); - reim_invctwiddle(&dre[1], &dim[1], &dre[9], &dim[9], omre, omim); - reim_invctwiddle(&dre[2], &dim[2], &dre[10], &dim[10], omre, omim); - reim_invctwiddle(&dre[3], &dim[3], &dre[11], &dim[11], omre, omim); - reim_invctwiddle(&dre[4], &dim[4], &dre[12], &dim[12], omre, omim); - reim_invctwiddle(&dre[5], &dim[5], &dre[13], &dim[13], omre, omim); - reim_invctwiddle(&dre[6], &dim[6], &dre[14], &dim[14], omre, omim); - reim_invctwiddle(&dre[7], &dim[7], &dre[15], &dim[15], omre, omim); - } -} - -void fill_reim_ifft16_omegas(const double entry_pwr, double** omg) { - const double j_pow = 1. / 8.; - const double k_pow = 1. / 16.; - const double pin = entry_pwr / 2.; - const double pin_2 = entry_pwr / 4.; - const double pin_4 = entry_pwr / 8.; - const double pin_8 = entry_pwr / 16.; - // ((8,9,10,11),(12,13,14,15)) are 4 reals then 4 imag of om^1/8*(1,k,j,kj) - (*omg)[0] = cos(2. * M_PI * (pin_8)); - (*omg)[1] = cos(2. * M_PI * (pin_8 + j_pow)); - (*omg)[2] = cos(2. * M_PI * (pin_8 + k_pow)); - (*omg)[3] = cos(2. * M_PI * (pin_8 + j_pow + k_pow)); - (*omg)[4] = - sin(2. * M_PI * (pin_8)); - (*omg)[5] = - sin(2. * M_PI * (pin_8 + j_pow)); - (*omg)[6] = - sin(2. * M_PI * (pin_8 + k_pow)); - (*omg)[7] = - sin(2. * M_PI * (pin_8 + j_pow + k_pow)); - // (4,5) and (6,7) are real and imag of om^1/4 and j.om^1/4 - (*omg)[8] = cos(2. * M_PI * (pin_4)); - (*omg)[9] = - sin(2. * M_PI * (pin_4)); - (*omg)[10] = cos(2. * M_PI * (pin_4 + j_pow)); - (*omg)[11] = - sin(2. * M_PI * (pin_4 + j_pow)); - // 2 and 3 are real and imag of om^1/2 - (*omg)[12] = cos(2. * M_PI * (pin_2)); - (*omg)[13] = - sin(2. * M_PI * (pin_2)); - // 0 and 1 are real and imag of om - (*omg)[14] = cos(2. * M_PI * pin); - (*omg)[15] = - sin(2. * M_PI * pin); - *omg += 16; -} - -void reim_ifft8_ref(double* dre, double* dim, const void* pom) { - const double* om = (const double*)pom; - { - double omare = om[0]; - double ombre = om[1]; - double omaim = om[2]; - double ombim = om[3]; - reim_invctwiddle(&dre[0], &dim[0], &dre[1], &dim[1], omare, omaim); - reim_invcitwiddle(&dre[2], &dim[2], &dre[3], &dim[3], omare, omaim); - reim_invctwiddle(&dre[4], &dim[4], &dre[5], &dim[5], ombre, ombim); - reim_invcitwiddle(&dre[6], &dim[6], &dre[7], &dim[7], ombre, ombim); - } - { - double omare = om[4]; - double omaim = om[5]; - reim_invctwiddle(&dre[0], &dim[0], &dre[2], &dim[2], omare, omaim); - reim_invctwiddle(&dre[1], &dim[1], &dre[3], &dim[3], omare, omaim); - reim_invcitwiddle(&dre[4], &dim[4], &dre[6], &dim[6], omare, omaim); - reim_invcitwiddle(&dre[5], &dim[5], &dre[7], &dim[7], omare, omaim); - } - { - double omre = om[6]; - double omim = om[7]; - reim_invctwiddle(&dre[0], &dim[0], &dre[4], &dim[4], omre, omim); - reim_invctwiddle(&dre[1], &dim[1], &dre[5], &dim[5], omre, omim); - reim_invctwiddle(&dre[2], &dim[2], &dre[6], &dim[6], omre, omim); - reim_invctwiddle(&dre[3], &dim[3], &dre[7], &dim[7], omre, omim); - } -} - -void fill_reim_ifft8_omegas(const double entry_pwr, double** omg) { - const double j_pow = 1. / 8.; - const double pin = entry_pwr / 2.; - const double pin_2 = entry_pwr / 4.; - const double pin_4 = entry_pwr / 8.; - // (4,5) and (6,7) are real and imag of om^1/4 and j.om^1/4 - (*omg)[0] = cos(2. * M_PI * (pin_4)); - (*omg)[1] = cos(2. * M_PI * (pin_4 + j_pow)); - (*omg)[2] = - sin(2. * M_PI * (pin_4)); - (*omg)[3] = - sin(2. * M_PI * (pin_4 + j_pow)); - // 2 and 3 are real and imag of om^1/2 - (*omg)[4] = cos(2. * M_PI * (pin_2)); - (*omg)[5] = - sin(2. * M_PI * (pin_2)); - // 0 and 1 are real and imag of om - (*omg)[6] = cos(2. * M_PI * pin); - (*omg)[7] = - sin(2. * M_PI * pin); - *omg += 8; -} - -void reim_ifft4_ref(double* dre, double* dim, const void* pom) { - const double* om = (const double*)pom; - { - double omare = om[0]; - double omaim = om[1]; - reim_invctwiddle(&dre[0], &dim[0], &dre[1], &dim[1], omare, omaim); - reim_invcitwiddle(&dre[2], &dim[2], &dre[3], &dim[3], omare, omaim); - } - { - double omare = om[2]; - double omaim = om[3]; - reim_invctwiddle(&dre[0], &dim[0], &dre[2], &dim[2], omare, omaim); - reim_invctwiddle(&dre[1], &dim[1], &dre[3], &dim[3], omare, omaim); - } -} - -void fill_reim_ifft4_omegas(const double entry_pwr, double** omg) { - const double pin = entry_pwr / 2.; - const double pin_2 = entry_pwr / 4.; - // 2 and 3 are real and imag of om^1/2 - (*omg)[0] = cos(2. * M_PI * (pin_2)); - (*omg)[1] = - sin(2. * M_PI * (pin_2)); - // 0 and 1 are real and imag of om - (*omg)[2] = cos(2. * M_PI * pin); - (*omg)[3] = - sin(2. * M_PI * pin); - *omg += 4; -} - -void reim_ifft2_ref(double* dre, double* dim, const void* pom) { - const double* om = (const double*)pom; - { - double omare = om[0]; - double omaim = om[1]; - reim_invctwiddle(&dre[0], &dim[0], &dre[1], &dim[1], omare, omaim); - } -} - -void fill_reim_ifft2_omegas(const double entry_pwr, double** omg) { - const double pin = entry_pwr / 2.; - // 0 and 1 are real and imag of om - (*omg)[0] = cos(2. * M_PI * pin); - (*omg)[1] = - sin(2. * M_PI * pin); - *omg += 2; -} - -void reim_invtwiddle_ifft_ref(uint64_t h, double* re, double* im, double om[2]) { - for (uint64_t i=0; i<h; ++i) { - reim_invctwiddle(&re[i],&im[i],&re[h+i],&im[h+i], om[0], om[1]); - } -} - -void reim_invbitwiddle_ifft_ref(uint64_t h, double* re, double* im, double om[4]) { - double* r0 = re; - double* r1 = re + h; - double* r2 = re + 2*h; - double* r3 = re + 3*h; - double* i0 = im; - double* i1 = im + h; - double* i2 = im + 2*h; - double* i3 = im + 3*h; - for (uint64_t i=0; i<h; ++i) { - reim_invctwiddle(&r0[i],&i0[i],&r1[i],&i1[i], om[0], om[1]); - reim_invcitwiddle(&r2[i],&i2[i],&r3[i],&i3[i], om[0], om[1]); - } - for (uint64_t i=0; i<h; ++i) { - reim_invctwiddle(&r0[i],&i0[i],&r2[i],&i2[i], om[2], om[3]); - reim_invctwiddle(&r1[i],&i1[i],&r3[i],&i3[i], om[2], om[3]); - } -} - -void reim_ifft_bfs_16_ref(uint64_t m, double* re, double* im, double** omg) { - uint64_t log2m = log2(m); - for (uint64_t off = 0; off < m; off += 16) { - reim_ifft16_ref(re+off, im+off, *omg); - *omg += 16; - } - uint64_t h = 16; - uint64_t ms2 = m/2; - while (h < ms2) { - uint64_t mm = h << 2; - for (uint64_t off = 0; off < m; off += mm) { - reim_invbitwiddle_ifft_ref(h, re + off, im + off, *omg); - *omg += 4; - } - h = mm; - } - if (log2m % 2 != 0) { - if (h!=ms2) abort(); // bug - // do the first twiddle iteration normally - reim_invtwiddle_ifft_ref(h, re, im, *omg); - *omg += 2; - h = m; - } -} - -void fill_reim_ifft_bfs_16_omegas(uint64_t m, double entry_pwr, double** omg) { - uint64_t log2m = log2(m); - //uint64_t mm = 16; - double ss = entry_pwr * 16. / m; - for (uint64_t off = 0; off < m; off += 16) { - double s = ss + fracrevbits(off/16); - fill_reim_ifft16_omegas(s, omg); - } - uint64_t h = 16; - uint64_t ms2 = m/2; - while (h < ms2) { - uint64_t mm = h << 2; - for (uint64_t off = 0; off < m; off += mm) { - double rs0 = ss + fracrevbits(off / mm) / 4.; - double rs1 = 2. * rs0; - (*omg)[0] = cos(2 * M_PI * rs0); - (*omg)[1] = -sin(2 * M_PI * rs0); - (*omg)[2] = cos(2 * M_PI * rs1); - (*omg)[3] = -sin(2 * M_PI * rs1); - *omg += 4; - } - ss *= 4.; - h = mm; - } - if (log2m % 2 != 0) { - if (h!=ms2) abort(); // bug - // do the first twiddle iteration normally - (*omg)[0] = cos(2 * M_PI * ss); - (*omg)[1] = - sin(2 * M_PI * ss); - *omg += 2; - h = m; - ss *= 2.; - } - if (ss!=entry_pwr) abort(); -} - -void reim_ifft_rec_16_ref(uint64_t m, double* re, double* im, double** omg) { - if (m <= 2048) return reim_ifft_bfs_16_ref(m, re, im, omg); - const uint32_t h = m / 2; - reim_ifft_rec_16_ref(h, re, im, omg); - reim_ifft_rec_16_ref(h, re + h, im + h, omg); - reim_invtwiddle_ifft_ref(h, re, im, *omg); - *omg += 2; -} - -void fill_reim_ifft_rec_16_omegas(uint64_t m, double entry_pwr, double** omg) { - if (m <= 2048) return fill_reim_ifft_bfs_16_omegas(m, entry_pwr, omg); - const uint64_t h = m / 2; - const double s = entry_pwr / 2; - fill_reim_ifft_rec_16_omegas(h, s, omg); - fill_reim_ifft_rec_16_omegas(h, s + 0.5, omg); - (*omg)[0] = cos(2 * M_PI * s); - (*omg)[1] = - sin(2 * M_PI * s); - *omg += 2; -} - -void reim_ifft_ref(const REIM_IFFT_PRECOMP* precomp, double* dat) { - const int32_t m = precomp->m; - double* omg = precomp->powomegas; - double* re = dat; - double* im = dat+m; - if (m <= 16) { - switch (m) { - case 1: - return; - case 2: - return reim_ifft2_ref(re, im, omg); - case 4: - return reim_ifft4_ref(re, im, omg); - case 8: - return reim_ifft8_ref(re, im, omg); - case 16: - return reim_ifft16_ref(re, im, omg); - default: - abort(); // m is not a power of 2 - } - } - if (m <= 2048) return reim_ifft_bfs_16_ref(m, re, im, &omg); - return reim_ifft_rec_16_ref(m, re, im, &omg); -} - -EXPORT REIM_IFFT_PRECOMP* new_reim_ifft_precomp(uint32_t m, uint32_t num_buffers) { - const uint64_t OMG_SPACE = ceilto64b(2 * m * sizeof(double)); - const uint64_t BUF_SIZE = ceilto64b(2 * m * sizeof(double)); - void* reps = malloc(sizeof(REIM_IFFT_PRECOMP) // base - + 63 // padding - + OMG_SPACE // tables //TODO 16? - + num_buffers * BUF_SIZE // buffers - ); - uint64_t aligned_addr = ceilto64b((uint64_t)(reps) + sizeof(REIM_IFFT_PRECOMP)); - REIM_IFFT_PRECOMP* r = (REIM_IFFT_PRECOMP*)reps; - r->m = m; - r->buf_size = BUF_SIZE; - r->powomegas = (double*)aligned_addr; - r->aligned_buffers = (void*)(aligned_addr + OMG_SPACE); - // fill in powomegas - double* omg = (double*) r->powomegas; - if (m <= 16) { - switch (m) { - case 1: - break; - case 2: - fill_reim_ifft2_omegas(0.25, &omg); - break; - case 4: - fill_reim_ifft4_omegas(0.25, &omg); - break; - case 8: - fill_reim_ifft8_omegas(0.25, &omg); - break; - case 16: - fill_reim_ifft16_omegas(0.25, &omg); - break; - default: - abort(); // m is not a power of 2 - } - } else if (m <= 2048) { - fill_reim_ifft_bfs_16_omegas(m, 0.25, &omg); - } else { - fill_reim_ifft_rec_16_omegas(m, 0.25, &omg); - } - if (((uint64_t)omg) - aligned_addr > OMG_SPACE) abort(); - // dispatch the right implementation - { - if (CPU_SUPPORTS("fma")) { - r->function = reim_ifft_avx2_fma; - } else { - r->function = reim_ifft_ref; - } - } - return reps; -} - - -void reim_naive_ifft(uint64_t m, double entry_pwr, double* re, double* im) { - if (m == 1) return; - // twiddle - const uint64_t h = m / 2; - const double s = entry_pwr / 2.; - reim_naive_ifft(h, s, re, im); - reim_naive_ifft(h, s + 0.5, re + h, im + h); - const double sre = cos(2 * M_PI * s); - const double sim = - sin(2 * M_PI * s); - for (uint64_t j = 0; j < h; ++j) { - double rdiff = re[j] - re[h+j]; - double idiff = im[j] - im[h+j]; - re[j] = re[j] + re[h+j]; - im[j] = im[j] + im[h+j]; - re[h+j] = rdiff * sre - idiff * sim; - im[h + j] = idiff * sre + rdiff * sim; - } -}
D spqlios/lib/spqlios/reim/reim_to_tnx_avx.c

@@ -1,32 +0,0 @@

-#include <errno.h> -#include <immintrin.h> -#include <string.h> - -#include "../commons_private.h" -#include "reim_fft_internal.h" -#include "reim_fft_private.h" - -typedef union {double d; uint64_t u;} dblui64_t; - -EXPORT void reim_to_tnx_avx(const REIM_TO_TNX_PRECOMP* tables, double* r, const double* x) { - const uint64_t n = tables->m << 1; - const __m256d add_cst = _mm256_set1_pd(tables->add_cst); - const __m256d mask_and = _mm256_castsi256_pd(_mm256_set1_epi64x(tables->mask_and)); - const __m256d mask_or = _mm256_castsi256_pd(_mm256_set1_epi64x(tables->mask_or)); - const __m256d sub_cst = _mm256_set1_pd(tables->sub_cst); - __m256d reg0,reg1; - for (uint64_t i=0; i<n; i+=8) { - reg0 = _mm256_loadu_pd(x+i); - reg1 = _mm256_loadu_pd(x+i+4); - reg0 = _mm256_add_pd(reg0, add_cst); - reg1 = _mm256_add_pd(reg1, add_cst); - reg0 = _mm256_and_pd(reg0, mask_and); - reg1 = _mm256_and_pd(reg1, mask_and); - reg0 = _mm256_or_pd(reg0, mask_or); - reg1 = _mm256_or_pd(reg1, mask_or); - reg0 = _mm256_sub_pd(reg0, sub_cst); - reg1 = _mm256_sub_pd(reg1, sub_cst); - _mm256_storeu_pd(r+i, reg0); - _mm256_storeu_pd(r+i+4, reg1); - } -}
D spqlios/lib/spqlios/reim/reim_to_tnx_ref.c

@@ -1,72 +0,0 @@

-#include <errno.h> -#include <string.h> - -#include "../commons_private.h" -#include "reim_fft_internal.h" -#include "reim_fft_private.h" - -EXPORT void reim_to_tnx_basic_ref(const REIM_TO_TNX_PRECOMP* tables, double* r, const double* x) { - const uint64_t n = tables->m << 1; - double divisor = tables->divisor; - for (uint64_t i=0; i<n; ++i) { - double ri = x[i]/divisor; - r[i] = ri - rint(ri); - } -} - -typedef union {double d; uint64_t u;} dblui64_t; - -EXPORT void reim_to_tnx_ref(const REIM_TO_TNX_PRECOMP* tables, double* r, const double* x) { - const uint64_t n = tables->m << 1; - double add_cst = tables->add_cst; - uint64_t mask_and = tables->mask_and; - uint64_t mask_or = tables->mask_or; - double sub_cst = tables->sub_cst; - dblui64_t cur; - for (uint64_t i=0; i<n; ++i) { - cur.d = x[i] + add_cst; - cur.u &= mask_and; - cur.u |= mask_or; - r[i] = cur.d - sub_cst; - } -} - -void* init_reim_to_tnx_precomp(REIM_TO_TNX_PRECOMP* const res, uint32_t m, double divisor, uint32_t log2overhead) { - if (m & (m - 1)) return spqlios_error("m must be a power of 2"); - if (is_not_pow2_double(&divisor)) return spqlios_error("divisor must be a power of 2"); - if (log2overhead > 52) return spqlios_error("log2overhead is too large"); - res->m = m; - res->divisor = divisor; - res->log2overhead = log2overhead; - // 52 + 11 + 1 - // ......1.......01(1)|expo|sign - // .......=========(1)|expo|sign msbbits = log2ovh + 2 + 11 + 1 - uint64_t nbits = 50 - log2overhead; - dblui64_t ovh_cst; - ovh_cst.d = 0.5 + (6<<log2overhead); - res->add_cst = ovh_cst.d * divisor; - res->mask_and = ((UINT64_C(1) << nbits) - 1); - res->mask_or = ovh_cst.u & ((UINT64_C(-1)) << nbits); - res->sub_cst = ovh_cst.d; - // TODO: check selection logic - if (CPU_SUPPORTS("avx2")) { - if (m >= 8) { - res->function = reim_to_tnx_avx; - } else { - res->function = reim_to_tnx_ref; - } - } else { - res->function = reim_to_tnx_ref; - } - return res; -} - -EXPORT REIM_TO_TNX_PRECOMP* new_reim_to_tnx_precomp(uint32_t m, double divisor, uint32_t log2overhead) { - REIM_TO_TNX_PRECOMP* res = malloc(sizeof(*res)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_reim_to_tnx_precomp(res, m, divisor, log2overhead)); -} - -EXPORT void reim_to_tnx(const REIM_TO_TNX_PRECOMP* tables, double* r, const double* x) { - tables->function(tables, r, x); -}
D spqlios/lib/spqlios/reim4/reim4_arithmetic.h

@@ -1,149 +0,0 @@

-#ifndef SPQLIOS_REIM4_ARITHMETIC_H -#define SPQLIOS_REIM4_ARITHMETIC_H - -#include "../commons.h" - -// the reim4 structure represent 4 complex numbers, -// represented as re0,re1,re2,re3,im0,im1,im2,im3 - -// TODO implement these basic arithmetic functions. (only ref is needed) - -/** @brief dest = 0 */ -EXPORT void reim4_zero(double* dest); -/** @brief dest = a + b */ -EXPORT void reim4_add(double* dest, const double* u, const double* v); -/** @brief dest = a * b */ -EXPORT void reim4_mul(double* dest, const double* u, const double* v); -/** @brief dest += a * b */ -EXPORT void reim4_add_mul(double* dest, const double* a, const double* b); - -// TODO add the dot products: vec x mat1col, vec x mat2cols functions here (ref and avx) - -// TODO implement the convolution functions here (ref and avx) -/** - * @brief k-th coefficient of the convolution product a * b. - * - * The k-th coefficient is defined as sum_{i+j=k} a[i].b[j]. - * (in this sum, i and j must remain within the bounds [0,sizea[ and [0,sizeb[) - * - * In practice, accounting for these bounds, the convolution function boils down to - * ``` - * res := 0 - * if (k<sizeb+sizea-1) then - * for j from max(0,k+1-sizea) incl to min(sizeb,k+1) excl do - * res += a[k-j] * b[j] - * return res - * ``` - */ -EXPORT void reim4_convolution_1coeff_ref(uint64_t k, double* dest, const double* a, uint64_t sizea, const double* b, - uint64_t sizeb); -EXPORT void reim4_convolution_1coeff_avx(uint64_t k, double* dest, const double* a, uint64_t sizea, const double* b, - uint64_t sizeb); -/** @brief returns two consecutive convolution coefficients: k and k+1 */ -EXPORT void reim4_convolution_2coeff_ref(uint64_t k, double* dest, const double* a, uint64_t sizea, const double* b, - uint64_t sizeb); -EXPORT void reim4_convolution_2coeff_avx(uint64_t k, double* dest, const double* a, uint64_t sizea, const double* b, - uint64_t sizeb); - -/** - * @brief From the convolution a * b, return the coefficients between offset and offset + size - * For the full convolution, use offset=0 and size=sizea+sizeb-1. - */ -EXPORT void reim4_convolution_ref(double* dest, uint64_t dest_size, uint64_t dest_offset, const double* a, - uint64_t sizea, const double* b, uint64_t sizeb); -EXPORT void reim4_convolution_avx(double* dest, uint64_t dest_size, uint64_t dest_offset, const double* a, - uint64_t sizea, const double* b, uint64_t sizeb); - -// The reim fft layout that encodes m = nn/2 complex evaluations -// can be decomposed into m/4 reim4 blocks -// the i-th block of v is noted v(i); -// it contains the 4 evaluations (e[4i],e[4i+1],e[4i+2],e[4i+3]) - -/** - * @brief extract 1 reim4 block from one reim vectors of m complexes - * @param m the size of each reim - * @param blk the block id to extract (<m/4) - * @param dst the output: nrows reim4's dst[i] = src[i](blk) - * @param src the input: nrows reim's - */ -EXPORT void reim4_extract_1blk_from_reim_ref(uint64_t m, uint64_t blk, - double* const dst, // 8 doubles - const double* const src // a reim vector -); -EXPORT void reim4_extract_1blk_from_reim_avx(uint64_t m, uint64_t blk, - double* const dst, // 8 doubles - const double* const src // a reim vectors -); - -/** - * @brief extract 1 reim4 block from nrows reim vectors of m complexes - * @param nrows the number of reim (fft) vectors - * @param m the size of each reim - * @param blk the block id to extract (<m/4) - * @param dst the output: nrows reim4's dst[i] = src[i](blk) - * @param src the input: nrows reim's - */ -EXPORT void reim4_extract_1blk_from_contiguous_reim_ref(uint64_t m, uint64_t nrows, uint64_t blk, double* const dst, - const double* const src); -EXPORT void reim4_extract_1blk_from_contiguous_reim_avx(uint64_t m, uint64_t nrows, uint64_t blk, double* const dst, - const double* const src); - -/** - * @brief extract 1 reim4 block from nrows reim vectors of m complexes with slice sl - * @param m the size of each reim - * @param sl the slice size - * @param nrows the number of reim (fft) vectors to extract - * @param blk the block id to extract (<m/4) - * @param dst the output: nrows reim4's dst[i] = src[i](blk) - * @param src the input: nrows reim's - */ -EXPORT void reim4_extract_1blk_from_contiguous_reim_sl_ref(uint64_t m, uint64_t sl, uint64_t nrows, uint64_t blk, - double* const dst, const double* const src); -EXPORT void reim4_extract_1blk_from_contiguous_reim_sl_avx(uint64_t m, uint64_t sl, uint64_t nrows, uint64_t blk, - double* const dst, const double* const src); - -/** - * @brief saves 1 single reim4 block in a reim vectors of m complexes - * @param m the size of each reim - * @param blk the block id to save (<m/4) - * @param dest the output reim: dst(blk) = src - * @param src the input reim4 - */ -EXPORT void reim4_save_1blk_to_reim_ref(uint64_t m, uint64_t blk, - double* dest, // 1 reim vector of length m - const double* src // 8 doubles -); -EXPORT void reim4_save_1blk_to_reim_avx(uint64_t m, uint64_t blk, - double* dest, // 1 reim vector of length m - const double* src // 8 doubles -); - -// dest = sum u[i].v[i] -// make ref and avx2 implem, needs to be fast -// can accumulate the real and imaginary parts on 2 separate registers, and combine at the end -EXPORT void reim4_vec_mat1col_product_ref(const uint64_t nrows, - double* const dst, // 8 doubles - const double* const u, // nrows * 8 doubles - const double* const v // nrows * 8 doubles -); - -EXPORT void reim4_vec_mat1col_product_avx2(const uint64_t nrows, - double* const dst, // 8 doubles - const double* const u, // nrows * 8 doubles - const double* const v // nrows * 8 doubles -); - -// dest = sum u[i].v[i] -// make ref and avx2 implem, needs to be fast -// can accumulate the real and imaginary parts on 2 separate registers, and recombine at the end -EXPORT void reim4_vec_mat2cols_product_ref(uint64_t nrows, double* const dst, // 16 doubles - const double* const u, // nrows * 8 doubles - const double* const v // nrows * 16 doubles -); - -EXPORT void reim4_vec_mat2cols_product_avx2(uint64_t nrows, double* const dst, // 16 doubles - const double* const u, // nrows * 8 doubles - const double* const v // nrows * 16 doubles -); - -#endif // SPQLIOS_REIM4_ARITHMETIC_H
D spqlios/lib/spqlios/reim4/reim4_arithmetic_avx2.c

@@ -1,130 +0,0 @@

-#include <assert.h> -#include <immintrin.h> - -#include "reim4_arithmetic.h" - -void reim4_extract_1blk_from_reim_avx(uint64_t m, uint64_t blk, - double* const dst, // nrows * 8 doubles - const double* const src // a contiguous array of nrows reim vectors -) { - assert(blk < (m >> 2)); - const double* src_ptr = src + (blk << 2); - double* dst_ptr = dst; - _mm256_storeu_pd(dst_ptr, _mm256_loadu_pd(src_ptr)); - _mm256_storeu_pd(dst_ptr + 4, _mm256_loadu_pd(src_ptr + m)); -} - -void reim4_extract_1blk_from_contiguous_reim_avx(uint64_t m, uint64_t nrows, uint64_t blk, double* const dst, - const double* const src) { - assert(blk < (m >> 2)); - const double* src_ptr = src + (blk << 2); - double* dst_ptr = dst; - for (uint64_t i = 0; i < nrows * 2; ++i) { - _mm256_storeu_pd(dst_ptr, _mm256_loadu_pd(src_ptr)); - dst_ptr += 4; - src_ptr += m; - } -} - -EXPORT void reim4_extract_1blk_from_contiguous_reim_sl_avx(uint64_t m, uint64_t sl, uint64_t nrows, uint64_t blk, - double* const dst, const double* const src) { - assert(blk < (m >> 2)); - const double* src_ptr = src + (blk << 2); - double* dst_ptr = dst; - for (uint64_t i = 0; i < nrows; ++i) { - _mm256_storeu_pd(dst_ptr, _mm256_loadu_pd(src_ptr)); - _mm256_storeu_pd(dst_ptr + 4, _mm256_loadu_pd(src_ptr + m)); - dst_ptr += 8; - src_ptr += sl; - } -} - -void reim4_save_1blk_to_reim_avx(uint64_t m, uint64_t blk, - double* dst, // 1 reim vector of length m - const double* src // 8 doubles -) { - assert(blk < (m >> 2)); - const double* src_ptr = src; - double* dst_ptr = dst + (blk << 2); - _mm256_storeu_pd(dst_ptr, _mm256_loadu_pd(src_ptr)); - _mm256_storeu_pd(dst_ptr + m, _mm256_loadu_pd(src_ptr + 4)); -} - -__always_inline void cplx_prod(__m256d* re1, __m256d* re2, __m256d* im, const double* const u_ptr, - const double* const v_ptr) { - const __m256d a = _mm256_loadu_pd(u_ptr); - const __m256d c = _mm256_loadu_pd(v_ptr); - *re1 = _mm256_fmadd_pd(a, c, *re1); - - const __m256d b = _mm256_loadu_pd(u_ptr + 4); - *im = _mm256_fmadd_pd(b, c, *im); - - const __m256d d = _mm256_loadu_pd(v_ptr + 4); - *re2 = _mm256_fmadd_pd(b, d, *re2); - *im = _mm256_fmadd_pd(a, d, *im); -} - -void reim4_vec_mat1col_product_avx2(const uint64_t nrows, - double* const dst, // 8 doubles - const double* const u, // nrows * 8 doubles - const double* const v // nrows * 8 doubles -) { - __m256d re1 = _mm256_setzero_pd(); - __m256d re2 = _mm256_setzero_pd(); - __m256d im1 = _mm256_setzero_pd(); - __m256d im2 = _mm256_setzero_pd(); - - const double* u_ptr = u; - const double* v_ptr = v; - for (uint64_t i = 0; i < nrows; ++i) { - const __m256d a = _mm256_loadu_pd(u_ptr); - const __m256d c = _mm256_loadu_pd(v_ptr); - re1 = _mm256_fmadd_pd(a, c, re1); - - const __m256d b = _mm256_loadu_pd(u_ptr + 4); - im2 = _mm256_fmadd_pd(b, c, im2); - - const __m256d d = _mm256_loadu_pd(v_ptr + 4); - re2 = _mm256_fmadd_pd(b, d, re2); - im1 = _mm256_fmadd_pd(a, d, im1); - - u_ptr += 8; - v_ptr += 8; - } - - _mm256_storeu_pd(dst, _mm256_sub_pd(re1, re2)); - _mm256_storeu_pd(dst + 4, _mm256_add_pd(im1, im2)); -} - -EXPORT void reim4_vec_mat2cols_product_avx2(const uint64_t nrows, - double* const dst, // 16 doubles - const double* const u, // nrows * 16 doubles - const double* const v // nrows * 16 doubles -) { - __m256d re1 = _mm256_setzero_pd(); - __m256d im1 = _mm256_setzero_pd(); - __m256d re2 = _mm256_setzero_pd(); - __m256d im2 = _mm256_setzero_pd(); - - __m256d ur,ui,ar,ai,br,bi; - for (uint64_t i = 0; i < nrows; ++i) { - ur = _mm256_loadu_pd(u+8*i); - ui = _mm256_loadu_pd(u+8*i+4); - ar = _mm256_loadu_pd(v+16*i); - ai = _mm256_loadu_pd(v+16*i+4); - br = _mm256_loadu_pd(v+16*i+8); - bi = _mm256_loadu_pd(v+16*i+12); - re1 = _mm256_fmsub_pd(ui,ai,re1); - re2 = _mm256_fmsub_pd(ui,bi,re2); - im1 = _mm256_fmadd_pd(ur,ai,im1); - im2 = _mm256_fmadd_pd(ur,bi,im2); - re1 = _mm256_fmsub_pd(ur,ar,re1); - re2 = _mm256_fmsub_pd(ur,br,re2); - im1 = _mm256_fmadd_pd(ui,ar,im1); - im2 = _mm256_fmadd_pd(ui,br,im2); - } - _mm256_storeu_pd(dst, re1); - _mm256_storeu_pd(dst + 4, im1); - _mm256_storeu_pd(dst + 8, re2); - _mm256_storeu_pd(dst + 12, im2); -}
D spqlios/lib/spqlios/reim4/reim4_arithmetic_ref.c

@@ -1,214 +0,0 @@

-#include <assert.h> -#include <string.h> - -#include "reim4_arithmetic.h" - -void reim4_extract_1blk_from_reim_ref(uint64_t m, uint64_t blk, - double* const dst, // 8 doubles - const double* const src // one reim vector -) { - assert(blk < (m >> 2)); - const double* src_ptr = src + (blk << 2); - // copy the real parts - dst[0] = src_ptr[0]; - dst[1] = src_ptr[1]; - dst[2] = src_ptr[2]; - dst[3] = src_ptr[3]; - src_ptr += m; - // copy the imaginary parts - dst[4] = src_ptr[0]; - dst[5] = src_ptr[1]; - dst[6] = src_ptr[2]; - dst[7] = src_ptr[3]; -} - -EXPORT void reim4_extract_1blk_from_contiguous_reim_ref(uint64_t m, uint64_t nrows, uint64_t blk, double* const dst, - const double* const src) { - assert(blk < (m >> 2)); - - const double* src_ptr = src + (blk << 2); - double* dst_ptr = dst; - for (uint64_t i = 0; i < nrows * 2; ++i) { - dst_ptr[0] = src_ptr[0]; - dst_ptr[1] = src_ptr[1]; - dst_ptr[2] = src_ptr[2]; - dst_ptr[3] = src_ptr[3]; - dst_ptr += 4; - src_ptr += m; - } -} - -EXPORT void reim4_extract_1blk_from_contiguous_reim_sl_ref(uint64_t m, uint64_t sl, uint64_t nrows, uint64_t blk, - double* const dst, const double* const src) { - assert(blk < (m >> 2)); - - const double* src_ptr = src + (blk << 2); - double* dst_ptr = dst; - const uint64_t sl_minus_m = sl - m; - for (uint64_t i = 0; i < nrows; ++i) { - dst_ptr[0] = src_ptr[0]; - dst_ptr[1] = src_ptr[1]; - dst_ptr[2] = src_ptr[2]; - dst_ptr[3] = src_ptr[3]; - dst_ptr += 4; - src_ptr += m; - dst_ptr[0] = src_ptr[0]; - dst_ptr[1] = src_ptr[1]; - dst_ptr[2] = src_ptr[2]; - dst_ptr[3] = src_ptr[3]; - dst_ptr += 4; - src_ptr += sl_minus_m; - } -} - -// dest(i)=src -// use scalar or sse or avx? Code -// should be the inverse of reim4_extract_1col_from_reim -void reim4_save_1blk_to_reim_ref(uint64_t m, uint64_t blk, - double* dst, // 1 reim vector of length m - const double* src // 8 doubles -) { - assert(blk < (m >> 2)); - double* dst_ptr = dst + (blk << 2); - // save the real part - dst_ptr[0] = src[0]; - dst_ptr[1] = src[1]; - dst_ptr[2] = src[2]; - dst_ptr[3] = src[3]; - dst_ptr += m; - // save the imag part - dst_ptr[0] = src[4]; - dst_ptr[1] = src[5]; - dst_ptr[2] = src[6]; - dst_ptr[3] = src[7]; -} - -// dest = 0 -void reim4_zero(double* const dst // 8 doubles -) { - for (uint64_t i = 0; i < 8; ++i) dst[i] = 0; -} - -/** @brief dest = a + b */ -void reim4_add(double* const dst, // 8 doubles - const double* const u, // nrows * 8 doubles - const double* const v // nrows * 8 doubles -) { - for (uint64_t k = 0; k < 4; ++k) { - const double a = u[k]; - const double c = v[k]; - const double b = u[k + 4]; - const double d = v[k + 4]; - - dst[k] = a + c; - dst[k + 4] = b + d; - } -} - -/** @brief dest = a * b */ -void reim4_mul(double* const dst, // 8 doubles - const double* const u, // 8 doubles - const double* const v // 8 doubles -) { - for (uint64_t k = 0; k < 4; ++k) { - const double a = u[k]; - const double c = v[k]; - const double b = u[k + 4]; - const double d = v[k + 4]; - - dst[k] = a * c - b * d; - dst[k + 4] = a * d + b * c; - } -} - -/** @brief dest += a * b */ -void reim4_add_mul(double* const dst, // 8 doubles - const double* const u, // 8 doubles - const double* const v // 8 doubles -) { - for (uint64_t k = 0; k < 4; ++k) { - const double a = u[k]; - const double c = v[k]; - const double b = u[k + 4]; - const double d = v[k + 4]; - - dst[k] += a * c - b * d; - dst[k + 4] += a * d + b * c; - } -} - -/** dest = uT * v where u is a vector of size nrows, and v is a nrows x 1 matrix */ -void reim4_vec_mat1col_product_ref(const uint64_t nrows, - double* const dst, // 8 doubles - const double* const u, // nrows * 8 doubles - const double* const v // nrows * 8 doubles -) { - reim4_zero(dst); - - for (uint64_t i = 0, j = 0; i < nrows; ++i, j += 8) { - reim4_add_mul(dst, u + j, v + j); - } -} - -/** dest = uT * v where u is a vector of size nrows, and v is a nrows x 2 matrix */ -void reim4_vec_mat2cols_product_ref(const uint64_t nrows, - double* const dst, // 16 doubles - const double* const u, // nrows * 8 doubles - const double* const v // nrows * 16 doubles -) { - reim4_zero(dst); - reim4_zero(dst + 8); - - double* dst1 = dst; - double* dst2 = dst + 8; - - for (uint64_t i = 0, j = 0; i < nrows; ++i, j += 8) { - uint64_t double_j = j << 1; - reim4_add_mul(dst1, u + j, v + double_j); - reim4_add_mul(dst2, u + j, v + double_j + 8); - } -} - -/** - * @brief k-th coefficient of the convolution product a * b. - * - * The k-th coefficient is defined as sum_{i+j=k} a[i].b[j]. - * (in this sum, i and j must remain within the bounds [0,sizea[ and [0,sizeb[) - * - * In practice, accounting for these bounds, the convolution function boils down to - * ``` - * res := 0 - * if (k<sizeb+sizea-1) then - * for j from max(0,k+1-sizea) incl to min(sizeb,k+1) excl do - * res += a[k-j] * b[j] - * return res - * ``` - */ -EXPORT void reim4_convolution_1coeff_ref(uint64_t k, double* dest, const double* a, uint64_t sizea, const double* b, - uint64_t sizeb) { - reim4_zero(dest); - if (k >= sizea + sizeb) return; - uint64_t jmin = k >= sizea ? k + 1 - sizea : 0; - uint64_t jmax = k < sizeb ? k + 1 : sizeb; - for (uint64_t j = jmin; j < jmax; ++j) { - reim4_add_mul(dest, a + 8 * (k - j), b + 8 * j); - } -} - -/** @brief returns two consecutive convolution coefficients: k and k+1 */ -EXPORT void reim4_convolution_2coeff_ref(uint64_t k, double* dest, const double* a, uint64_t sizea, const double* b, - uint64_t sizeb) { - reim4_convolution_1coeff_ref(k, dest, a, sizea, b, sizeb); - reim4_convolution_1coeff_ref(k + 1, dest + 8, a, sizea, b, sizeb); -} - -/** - * @brief From the convolution a * b, return the coefficients between offset and offset + size - * For the full convolution, use offset=0 and size=sizea+sizeb-1. - */ -EXPORT void reim4_convolution_ref(double* dest, uint64_t dest_size, uint64_t dest_offset, const double* a, - uint64_t sizea, const double* b, uint64_t sizeb) { - for (uint64_t k = 0; k < dest_size; ++k) { - reim4_convolution_1coeff_ref(k + dest_offset, dest + 8 * k, a, sizea, b, sizeb); - } -}
D spqlios/lib/spqlios/reim4/reim4_execute.c

@@ -1,19 +0,0 @@

-#include "reim4_fftvec_internal.h" -#include "reim4_fftvec_private.h" - -EXPORT void reim4_fftvec_addmul(const REIM4_FFTVEC_ADDMUL_PRECOMP* tables, double* r, const double* a, - const double* b) { - tables->function(tables, r, a, b); -} - -EXPORT void reim4_fftvec_mul(const REIM4_FFTVEC_MUL_PRECOMP* tables, double* r, const double* a, const double* b) { - tables->function(tables, r, a, b); -} - -EXPORT void reim4_from_cplx(const REIM4_FROM_CPLX_PRECOMP* tables, double* r, const void* a) { - tables->function(tables, r, a); -} - -EXPORT void reim4_to_cplx(const REIM4_TO_CPLX_PRECOMP* tables, void* r, const double* a) { - tables->function(tables, r, a); -}
D spqlios/lib/spqlios/reim4/reim4_fallbacks_aarch64.c

@@ -1,11 +0,0 @@

-#include "reim4_fftvec_private.h" - -EXPORT void reim4_fftvec_mul_fma(const REIM4_FFTVEC_MUL_PRECOMP* tables, double* r, const double* a, - const double* b){UNDEFINED()} - -EXPORT void reim4_fftvec_addmul_fma(const REIM4_FFTVEC_ADDMUL_PRECOMP* tables, double* r, const double* a, - const double* b){UNDEFINED()} - -EXPORT void reim4_from_cplx_fma(const REIM4_FROM_CPLX_PRECOMP* tables, double* r, const void* a){UNDEFINED()} - -EXPORT void reim4_to_cplx_fma(const REIM4_TO_CPLX_PRECOMP* tables, void* r, const double* a){UNDEFINED()}
D spqlios/lib/spqlios/reim4/reim4_fftvec_addmul_fma.c

@@ -1,54 +0,0 @@

-#include <assert.h> -#include <immintrin.h> -#include <stdint.h> - -#include "reim4_fftvec_private.h" - -EXPORT void reim4_fftvec_addmul_fma(const REIM4_FFTVEC_ADDMUL_PRECOMP* tables, double* r_ptr, const double* a_ptr, - const double* b_ptr) { - const double* const rend_ptr = r_ptr + (tables->m << 1); - while (r_ptr != rend_ptr) { - __m256d rr = _mm256_loadu_pd(r_ptr); - __m256d ri = _mm256_loadu_pd(r_ptr + 4); - const __m256d ar = _mm256_loadu_pd(a_ptr); - const __m256d ai = _mm256_loadu_pd(a_ptr + 4); - const __m256d br = _mm256_loadu_pd(b_ptr); - const __m256d bi = _mm256_loadu_pd(b_ptr + 4); - - rr = _mm256_fmsub_pd(ai, bi, rr); - rr = _mm256_fmsub_pd(ar, br, rr); - ri = _mm256_fmadd_pd(ar, bi, ri); - ri = _mm256_fmadd_pd(ai, br, ri); - - _mm256_storeu_pd(r_ptr, rr); - _mm256_storeu_pd(r_ptr + 4, ri); - - r_ptr += 8; - a_ptr += 8; - b_ptr += 8; - } -} - -EXPORT void reim4_fftvec_mul_fma(const REIM4_FFTVEC_MUL_PRECOMP* tables, double* r_ptr, const double* a_ptr, - const double* b_ptr) { - const double* const rend_ptr = r_ptr + (tables->m << 1); - while (r_ptr != rend_ptr) { - const __m256d ar = _mm256_loadu_pd(a_ptr); - const __m256d ai = _mm256_loadu_pd(a_ptr + 4); - const __m256d br = _mm256_loadu_pd(b_ptr); - const __m256d bi = _mm256_loadu_pd(b_ptr + 4); - - const __m256d t1 = _mm256_mul_pd(ai, bi); - const __m256d t2 = _mm256_mul_pd(ar, bi); - - __m256d rr = _mm256_fmsub_pd(ar, br, t1); - __m256d ri = _mm256_fmadd_pd(ai, br, t2); - - _mm256_storeu_pd(r_ptr, rr); - _mm256_storeu_pd(r_ptr + 4, ri); - - r_ptr += 8; - a_ptr += 8; - b_ptr += 8; - } -}
D spqlios/lib/spqlios/reim4/reim4_fftvec_addmul_ref.c

@@ -1,97 +0,0 @@

-#include <errno.h> -#include <memory.h> -#include <stdlib.h> -#include <string.h> - -#include "../commons_private.h" -#include "reim4_fftvec_internal.h" -#include "reim4_fftvec_private.h" - -void* init_reim4_fftvec_addmul_precomp(REIM4_FFTVEC_ADDMUL_PRECOMP* res, uint32_t m) { - res->m = m; - if (CPU_SUPPORTS("fma")) { - if (m >= 2) { - res->function = reim4_fftvec_addmul_fma; - } else { - res->function = reim4_fftvec_addmul_ref; - } - } else { - res->function = reim4_fftvec_addmul_ref; - } - return res; -} - -EXPORT REIM4_FFTVEC_ADDMUL_PRECOMP* new_reim4_fftvec_addmul_precomp(uint32_t m) { - REIM4_FFTVEC_ADDMUL_PRECOMP* res = malloc(sizeof(*res)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_reim4_fftvec_addmul_precomp(res, m)); -} - -EXPORT void reim4_fftvec_addmul_ref(const REIM4_FFTVEC_ADDMUL_PRECOMP* precomp, double* r, const double* a, - const double* b) { - const uint64_t m = precomp->m; - for (uint64_t j = 0; j < m / 4; ++j) { - for (uint64_t i = 0; i < 4; ++i) { - double re = a[i] * b[i] - a[i + 4] * b[i + 4]; - double im = a[i] * b[i + 4] + a[i + 4] * b[i]; - r[i] += re; - r[i + 4] += im; - } - a += 8; - b += 8; - r += 8; - } -} - -EXPORT void reim4_fftvec_addmul_simple(uint32_t m, double* r, const double* a, const double* b) { - static REIM4_FFTVEC_ADDMUL_PRECOMP precomp[32]; - REIM4_FFTVEC_ADDMUL_PRECOMP* p = precomp + log2m(m); - if (!p->function) { - if (!init_reim4_fftvec_addmul_precomp(p, m)) abort(); - } - p->function(p, r, a, b); -} - -void* init_reim4_fftvec_mul_precomp(REIM4_FFTVEC_MUL_PRECOMP* res, uint32_t m) { - res->m = m; - if (CPU_SUPPORTS("fma")) { - if (m >= 4) { - res->function = reim4_fftvec_mul_fma; - } else { - res->function = reim4_fftvec_mul_ref; - } - } else { - res->function = reim4_fftvec_mul_ref; - } - return res; -} - -EXPORT REIM4_FFTVEC_MUL_PRECOMP* new_reim4_fftvec_mul_precomp(uint32_t m) { - REIM4_FFTVEC_MUL_PRECOMP* res = malloc(sizeof(*res)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_reim4_fftvec_mul_precomp(res, m)); -} - -EXPORT void reim4_fftvec_mul_ref(const REIM4_FFTVEC_MUL_PRECOMP* precomp, double* r, const double* a, const double* b) { - const uint64_t m = precomp->m; - for (uint64_t j = 0; j < m / 4; ++j) { - for (uint64_t i = 0; i < 4; ++i) { - double re = a[i] * b[i] - a[i + 4] * b[i + 4]; - double im = a[i] * b[i + 4] + a[i + 4] * b[i]; - r[i] = re; - r[i + 4] = im; - } - a += 8; - b += 8; - r += 8; - } -} - -EXPORT void reim4_fftvec_mul_simple(uint32_t m, double* r, const double* a, const double* b) { - static REIM4_FFTVEC_MUL_PRECOMP precomp[32]; - REIM4_FFTVEC_MUL_PRECOMP* p = precomp + log2m(m); - if (!p->function) { - if (!init_reim4_fftvec_mul_precomp(p, m)) abort(); - } - p->function(p, r, a, b); -}
D spqlios/lib/spqlios/reim4/reim4_fftvec_conv_fma.c

@@ -1,37 +0,0 @@

-#include <assert.h> -#include <immintrin.h> -#include <stdint.h> - -#include "reim4_fftvec_private.h" - -EXPORT void reim4_from_cplx_fma(const REIM4_FROM_CPLX_PRECOMP* tables, double* r_ptr, const void* a) { - const double* const rend_ptr = r_ptr + (tables->m << 1); - - const double* a_ptr = (double*)a; - while (r_ptr != rend_ptr) { - __m256d t1 = _mm256_loadu_pd(a_ptr); - __m256d t2 = _mm256_loadu_pd(a_ptr + 4); - - _mm256_storeu_pd(r_ptr, _mm256_unpacklo_pd(t1, t2)); - _mm256_storeu_pd(r_ptr + 4, _mm256_unpackhi_pd(t1, t2)); - - r_ptr += 8; - a_ptr += 8; - } -} - -EXPORT void reim4_to_cplx_fma(const REIM4_TO_CPLX_PRECOMP* tables, void* r, const double* a_ptr) { - const double* const aend_ptr = a_ptr + (tables->m << 1); - double* r_ptr = (double*)r; - - while (a_ptr != aend_ptr) { - __m256d t1 = _mm256_loadu_pd(a_ptr); - __m256d t2 = _mm256_loadu_pd(a_ptr + 4); - - _mm256_storeu_pd(r_ptr, _mm256_unpacklo_pd(t1, t2)); - _mm256_storeu_pd(r_ptr + 4, _mm256_unpackhi_pd(t1, t2)); - - r_ptr += 8; - a_ptr += 8; - } -}
D spqlios/lib/spqlios/reim4/reim4_fftvec_conv_ref.c

@@ -1,116 +0,0 @@

-#include <errno.h> -#include <memory.h> -#include <stdlib.h> -#include <string.h> - -#include "../commons_private.h" -#include "reim4_fftvec_internal.h" -#include "reim4_fftvec_private.h" - -EXPORT void reim4_from_cplx_ref(const REIM4_FROM_CPLX_PRECOMP* tables, double* r, const void* a) { - const double* x = (double*)a; - const uint64_t m = tables->m; - for (uint64_t i = 0; i < m / 4; ++i) { - double r0 = x[0]; - double i0 = x[1]; - double r1 = x[2]; - double i1 = x[3]; - double r2 = x[4]; - double i2 = x[5]; - double r3 = x[6]; - double i3 = x[7]; - r[0] = r0; - r[1] = r2; - r[2] = r1; - r[3] = r3; - r[4] = i0; - r[5] = i2; - r[6] = i1; - r[7] = i3; - x += 8; - r += 8; - } -} - -void* init_reim4_from_cplx_precomp(REIM4_FROM_CPLX_PRECOMP* res, uint32_t nn) { - res->m = nn / 2; - if (CPU_SUPPORTS("fma")) { - if (nn >= 4) { - res->function = reim4_from_cplx_fma; - } else { - res->function = reim4_from_cplx_ref; - } - } else { - res->function = reim4_from_cplx_ref; - } - return res; -} - -EXPORT REIM4_FROM_CPLX_PRECOMP* new_reim4_from_cplx_precomp(uint32_t m) { - REIM4_FROM_CPLX_PRECOMP* res = malloc(sizeof(*res)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_reim4_from_cplx_precomp(res, m)); -} - -EXPORT void reim4_from_cplx_simple(uint32_t m, double* r, const void* a) { - static REIM4_FROM_CPLX_PRECOMP precomp[32]; - REIM4_FROM_CPLX_PRECOMP* p = precomp + log2m(m); - if (!p->function) { - if (!init_reim4_from_cplx_precomp(p, m)) abort(); - } - p->function(p, r, a); -} - -EXPORT void reim4_to_cplx_ref(const REIM4_TO_CPLX_PRECOMP* tables, void* r, const double* a) { - double* y = (double*)r; - const uint64_t m = tables->m; - for (uint64_t i = 0; i < m / 4; ++i) { - double r0 = a[0]; - double r2 = a[1]; - double r1 = a[2]; - double r3 = a[3]; - double i0 = a[4]; - double i2 = a[5]; - double i1 = a[6]; - double i3 = a[7]; - y[0] = r0; - y[1] = i0; - y[2] = r1; - y[3] = i1; - y[4] = r2; - y[5] = i2; - y[6] = r3; - y[7] = i3; - a += 8; - y += 8; - } -} - -void* init_reim4_to_cplx_precomp(REIM4_TO_CPLX_PRECOMP* res, uint32_t m) { - res->m = m; - if (CPU_SUPPORTS("fma")) { - if (m >= 2) { - res->function = reim4_to_cplx_fma; - } else { - res->function = reim4_to_cplx_ref; - } - } else { - res->function = reim4_to_cplx_ref; - } - return res; -} - -EXPORT REIM4_TO_CPLX_PRECOMP* new_reim4_to_cplx_precomp(uint32_t m) { - REIM4_TO_CPLX_PRECOMP* res = malloc(sizeof(*res)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_reim4_to_cplx_precomp(res, m)); -} - -EXPORT void reim4_to_cplx_simple(uint32_t m, void* r, const double* a) { - static REIM4_TO_CPLX_PRECOMP precomp[32]; - REIM4_TO_CPLX_PRECOMP* p = precomp + log2m(m); - if (!p->function) { - if (!init_reim4_to_cplx_precomp(p, m)) abort(); - } - p->function(p, r, a); -}
D spqlios/lib/spqlios/reim4/reim4_fftvec_internal.h

@@ -1,20 +0,0 @@

-#ifndef SPQLIOS_REIM4_FFTVEC_INTERNAL_H -#define SPQLIOS_REIM4_FFTVEC_INTERNAL_H - -#include "reim4_fftvec_public.h" - -EXPORT void reim4_fftvec_mul_ref(const REIM4_FFTVEC_MUL_PRECOMP* tables, double* r, const double* a, const double* b); -EXPORT void reim4_fftvec_mul_fma(const REIM4_FFTVEC_MUL_PRECOMP* tables, double* r, const double* a, const double* b); - -EXPORT void reim4_fftvec_addmul_ref(const REIM4_FFTVEC_ADDMUL_PRECOMP* tables, double* r, const double* a, - const double* b); -EXPORT void reim4_fftvec_addmul_fma(const REIM4_FFTVEC_ADDMUL_PRECOMP* tables, double* r, const double* a, - const double* b); - -EXPORT void reim4_from_cplx_ref(const REIM4_FROM_CPLX_PRECOMP* tables, double* r, const void* a); -EXPORT void reim4_from_cplx_fma(const REIM4_FROM_CPLX_PRECOMP* tables, double* r, const void* a); - -EXPORT void reim4_to_cplx_ref(const REIM4_TO_CPLX_PRECOMP* tables, void* r, const double* a); -EXPORT void reim4_to_cplx_fma(const REIM4_TO_CPLX_PRECOMP* tables, void* r, const double* a); - -#endif // SPQLIOS_REIM4_FFTVEC_INTERNAL_H
D spqlios/lib/spqlios/reim4/reim4_fftvec_private.h

@@ -1,33 +0,0 @@

-#ifndef SPQLIOS_REIM4_FFTVEC_PRIVATE_H -#define SPQLIOS_REIM4_FFTVEC_PRIVATE_H - -#include "reim4_fftvec_public.h" - -#define STATIC_ASSERT(condition) (void)sizeof(char[-1 + 2 * !!(condition)]) - -typedef void (*R4_FFTVEC_MUL_FUNC)(const REIM4_FFTVEC_MUL_PRECOMP*, double*, const double*, const double*); -typedef void (*R4_FFTVEC_ADDMUL_FUNC)(const REIM4_FFTVEC_ADDMUL_PRECOMP*, double*, const double*, const double*); -typedef void (*R4_FROM_CPLX_FUNC)(const REIM4_FROM_CPLX_PRECOMP*, double*, const void*); -typedef void (*R4_TO_CPLX_FUNC)(const REIM4_TO_CPLX_PRECOMP*, void*, const double*); - -struct reim4_mul_precomp { - R4_FFTVEC_MUL_FUNC function; - int64_t m; -}; - -struct reim4_addmul_precomp { - R4_FFTVEC_ADDMUL_FUNC function; - int64_t m; -}; - -struct reim4_from_cplx_precomp { - R4_FROM_CPLX_FUNC function; - int64_t m; -}; - -struct reim4_to_cplx_precomp { - R4_TO_CPLX_FUNC function; - int64_t m; -}; - -#endif // SPQLIOS_REIM4_FFTVEC_PRIVATE_H
D spqlios/lib/spqlios/reim4/reim4_fftvec_public.h

@@ -1,59 +0,0 @@

-#ifndef SPQLIOS_REIM4_FFTVEC_PUBLIC_H -#define SPQLIOS_REIM4_FFTVEC_PUBLIC_H - -#include "../commons.h" - -typedef struct reim4_addmul_precomp REIM4_FFTVEC_ADDMUL_PRECOMP; -typedef struct reim4_mul_precomp REIM4_FFTVEC_MUL_PRECOMP; -typedef struct reim4_from_cplx_precomp REIM4_FROM_CPLX_PRECOMP; -typedef struct reim4_to_cplx_precomp REIM4_TO_CPLX_PRECOMP; - -EXPORT REIM4_FFTVEC_MUL_PRECOMP* new_reim4_fftvec_mul_precomp(uint32_t m); -EXPORT void reim4_fftvec_mul(const REIM4_FFTVEC_MUL_PRECOMP* tables, double* r, const double* a, const double* b); -#define delete_reim4_fftvec_mul_precomp free - -EXPORT REIM4_FFTVEC_ADDMUL_PRECOMP* new_reim4_fftvec_addmul_precomp(uint32_t nn); -EXPORT void reim4_fftvec_addmul(const REIM4_FFTVEC_ADDMUL_PRECOMP* tables, double* r, const double* a, const double* b); -#define delete_reim4_fftvec_addmul_precomp free - -/** - * @brief prepares a conversion from the cplx fftvec layout to the reim4 layout. - * @param m complex dimension m from C[X] mod X^m-i. - */ -EXPORT REIM4_FROM_CPLX_PRECOMP* new_reim4_from_cplx_precomp(uint32_t m); -EXPORT void reim4_from_cplx(const REIM4_FROM_CPLX_PRECOMP* tables, double* r, const void* a); -#define delete_reim4_from_cplx_precomp free - -/** - * @brief prepares a conversion from the reim4 fftvec layout to the cplx layout - * @param m the complex dimension m from C[X] mod X^m-i. - */ -EXPORT REIM4_TO_CPLX_PRECOMP* new_reim4_to_cplx_precomp(uint32_t m); -EXPORT void reim4_to_cplx(const REIM4_TO_CPLX_PRECOMP* tables, void* r, const double* a); -#define delete_reim4_to_cplx_precomp free - -/** - * @brief Simpler API for the fftvec multiplication function. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void reim4_fftvec_mul_simple(uint32_t m, double* r, const double* a, const double* b); - -/** - * @brief Simpler API for the fftvec addmul function. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void reim4_fftvec_addmul_simple(uint32_t m, double* r, const double* a, const double* b); - -/** - * @brief Simpler API for cplx conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void reim4_from_cplx_simple(uint32_t m, double* r, const void* a); - -/** - * @brief Simpler API for to cplx conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void reim4_to_cplx_simple(uint32_t m, void* r, const double* a); - -#endif // SPQLIOS_REIM4_FFTVEC_PUBLIC_H
D spqlios/lib/test/CMakeLists.txt

@@ -1,142 +0,0 @@

-set(CMAKE_CXX_STANDARD 17) - -set(test_incs ..) -set(gtest_libs) -set(benchmark_libs) -# searching for libgtest -find_path(gtest_inc NAMES gtest/gtest.h) -find_library(gtest NAMES gtest) -find_library(gtest_main REQUIRED NAMES gtest_main) -if (gtest_inc AND gtest AND gtest_main) - message(STATUS "Found gtest: I=${gtest_inc} L=${gtest},${gtest_main}") - set(test_incs ${test_incs} ${gtest_inc}) - set(gtest_libs ${gtest_libs} ${gtest} ${gtest_main} pthread) -else() - message(FATAL_ERROR "Libgtest not found (required if ENABLE_TESTING is on): I=${gtest_inc} L=${gtest},${gtest_main}") -endif() -# searching for libbenchmark -find_path(benchmark_inc NAMES benchmark/benchmark.h) -find_library(benchmark NAMES benchmark) -if (benchmark_inc AND benchmark) - message(STATUS "Found benchmark: I=${benchmark_inc} L=${benchmark}") - set(test_incs ${test_incs} ${benchmark_inc}) - set(benchmark_libs ${benchmark_libs} ${benchmark}) -else() - message(FATAL_ERROR "Libbenchmark not found (required if ENABLE_TESTING is on): I=${benchmark_inc} L=${benchmark}") -endif() -find_path(VALGRIND_DIR NAMES valgrind/valgrind.h) -if (VALGRIND_DIR) - message(STATUS "Found valgrind header ${VALGRIND_DIR}") -else () - # for now, we will fail if we don't find valgrind for tests - message(STATUS "CANNOT FIND valgrind header: ${VALGRIND_DIR}") -endif () - -add_library(spqlios-testlib SHARED - testlib/random.cpp - testlib/test_commons.h - testlib/test_commons.cpp - testlib/mod_q120.h - testlib/mod_q120.cpp - testlib/negacyclic_polynomial.cpp - testlib/negacyclic_polynomial.h - testlib/negacyclic_polynomial_impl.h - testlib/reim4_elem.cpp - testlib/reim4_elem.h - testlib/fft64_dft.cpp - testlib/fft64_dft.h - testlib/fft64_layouts.h - testlib/fft64_layouts.cpp - testlib/ntt120_layouts.cpp - testlib/ntt120_layouts.h - testlib/ntt120_dft.cpp - testlib/ntt120_dft.h - testlib/test_hash.cpp - testlib/sha3.h - testlib/sha3.c - testlib/polynomial_vector.h - testlib/polynomial_vector.cpp - testlib/vec_rnx_layout.h - testlib/vec_rnx_layout.cpp - testlib/zn_layouts.h - testlib/zn_layouts.cpp -) -if (VALGRIND_DIR) - target_include_directories(spqlios-testlib PRIVATE ${VALGRIND_DIR}) - target_compile_definitions(spqlios-testlib PRIVATE VALGRIND_MEM_TESTS) -endif () -target_link_libraries(spqlios-testlib libspqlios) - - - -# main unittest file -message(STATUS "${gtest_libs}") -set(UNITTEST_FILES - spqlios_test.cpp - spqlios_reim_conversions_test.cpp - spqlios_reim_test.cpp - spqlios_reim4_arithmetic_test.cpp - spqlios_cplx_test.cpp - spqlios_cplx_conversions_test.cpp - spqlios_q120_ntt_test.cpp - spqlios_q120_arithmetic_test.cpp - spqlios_coeffs_arithmetic_test.cpp - spqlios_vec_znx_big_test.cpp - spqlios_znx_small_test.cpp - spqlios_vmp_product_test.cpp - spqlios_vec_znx_dft_test.cpp - spqlios_svp_test.cpp - spqlios_svp_product_test.cpp - spqlios_vec_znx_test.cpp - spqlios_vec_rnx_test.cpp - spqlios_vec_rnx_vmp_test.cpp - spqlios_vec_rnx_conversions_test.cpp - spqlios_vec_rnx_ppol_test.cpp - spqlios_vec_rnx_approxdecomp_tnxdbl_test.cpp - spqlios_zn_approxdecomp_test.cpp - spqlios_zn_conversions_test.cpp - spqlios_zn_vmp_test.cpp - - -) - -add_executable(spqlios-test ${UNITTEST_FILES}) -target_link_libraries(spqlios-test spqlios-testlib libspqlios ${gtest_libs}) -target_include_directories(spqlios-test PRIVATE ${test_incs}) -add_test(NAME spqlios-test COMMAND spqlios-test) -if (WIN32) - # copy the dlls to the test directory - cmake_minimum_required(VERSION 3.26) - add_custom_command( - POST_BUILD - TARGET spqlios-test - COMMAND ${CMAKE_COMMAND} -E copy - -t $<TARGET_FILE_DIR:spqlios-test> $<TARGET_RUNTIME_DLLS:spqlios-testlib> $<TARGET_RUNTIME_DLLS:libspqlios> - COMMAND_EXPAND_LISTS - ) -endif() - -# benchmarks -add_executable(spqlios-cplx-fft-bench spqlios_cplx_fft_bench.cpp) -target_link_libraries(spqlios-cplx-fft-bench libspqlios ${benchmark_libs} pthread) -target_include_directories(spqlios-cplx-fft-bench PRIVATE ${test_incs}) - -if (X86 OR X86_WIN32) - add_executable(spqlios-q120-ntt-bench spqlios_q120_ntt_bench.cpp) - target_link_libraries(spqlios-q120-ntt-bench libspqlios ${benchmark_libs} pthread) - target_include_directories(spqlios-q120-ntt-bench PRIVATE ${test_incs}) - - add_executable(spqlios-q120-arithmetic-bench spqlios_q120_arithmetic_bench.cpp) - target_link_libraries(spqlios-q120-arithmetic-bench libspqlios ${benchmark_libs} pthread) - target_include_directories(spqlios-q120-arithmetic-bench PRIVATE ${test_incs}) -endif () - -if (X86 OR X86_WIN32) - add_executable(spqlios_reim4_arithmetic_bench spqlios_reim4_arithmetic_bench.cpp) - target_link_libraries(spqlios_reim4_arithmetic_bench ${benchmark_libs} libspqlios pthread) - target_include_directories(spqlios_reim4_arithmetic_bench PRIVATE ${test_incs}) -endif () - -if (DEVMODE_INSTALL) - install(TARGETS spqlios-testlib) -endif()
D spqlios/lib/test/spqlios_coeffs_arithmetic_test.cpp

@@ -1,488 +0,0 @@

-#include <gtest/gtest.h> -#include <sys/types.h> - -#include <cstdint> -#include <limits> -#include <random> -#include <vector> - -#include "../spqlios/coeffs/coeffs_arithmetic.h" -#include "test/testlib/mod_q120.h" -#include "testlib/negacyclic_polynomial.h" -#include "testlib/test_commons.h" - -/// tests of element-wise operations -template <typename T, typename F, typename G> -void test_elemw_op(F elemw_op, G poly_elemw_op) { - for (uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096}) { - polynomial<T> a = polynomial<T>::random(n); - polynomial<T> b = polynomial<T>::random(n); - polynomial<T> expect(n); - polynomial<T> actual(n); - // out of place - expect = poly_elemw_op(a, b); - elemw_op(n, actual.data(), a.data(), b.data()); - ASSERT_EQ(actual, expect); - // in place 1 - actual = polynomial<T>::random(n); - expect = poly_elemw_op(actual, b); - elemw_op(n, actual.data(), actual.data(), b.data()); - ASSERT_EQ(actual, expect); - // in place 2 - actual = polynomial<T>::random(n); - expect = poly_elemw_op(a, actual); - elemw_op(n, actual.data(), a.data(), actual.data()); - ASSERT_EQ(actual, expect); - // in place 3 - actual = polynomial<T>::random(n); - expect = poly_elemw_op(actual, actual); - elemw_op(n, actual.data(), actual.data(), actual.data()); - ASSERT_EQ(actual, expect); - } -} - -static polynomial<int64_t> poly_i64_add(const polynomial<int64_t>& u, polynomial<int64_t>& v) { return u + v; } -static polynomial<int64_t> poly_i64_sub(const polynomial<int64_t>& u, polynomial<int64_t>& v) { return u - v; } -TEST(coeffs_arithmetic, znx_add_i64_ref) { test_elemw_op<int64_t>(znx_add_i64_ref, poly_i64_add); } -TEST(coeffs_arithmetic, znx_sub_i64_ref) { test_elemw_op<int64_t>(znx_sub_i64_ref, poly_i64_sub); } -#ifdef __x86_64__ -TEST(coeffs_arithmetic, znx_add_i64_avx) { test_elemw_op<int64_t>(znx_add_i64_avx, poly_i64_add); } -TEST(coeffs_arithmetic, znx_sub_i64_avx) { test_elemw_op<int64_t>(znx_sub_i64_avx, poly_i64_sub); } -#endif - -/// tests of element-wise operations -template <typename T, typename F, typename G> -void test_elemw_unary_op(F elemw_op, G poly_elemw_op) { - for (uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096}) { - polynomial<T> a = polynomial<T>::random(n); - polynomial<T> expect(n); - polynomial<T> actual(n); - // out of place - expect = poly_elemw_op(a); - elemw_op(n, actual.data(), a.data()); - ASSERT_EQ(actual, expect); - // in place - actual = polynomial<T>::random(n); - expect = poly_elemw_op(actual); - elemw_op(n, actual.data(), actual.data()); - ASSERT_EQ(actual, expect); - } -} - -static polynomial<int64_t> poly_i64_neg(const polynomial<int64_t>& u) { return -u; } -static polynomial<int64_t> poly_i64_copy(const polynomial<int64_t>& u) { return u; } -TEST(coeffs_arithmetic, znx_neg_i64_ref) { test_elemw_unary_op<int64_t>(znx_negate_i64_ref, poly_i64_neg); } -TEST(coeffs_arithmetic, znx_copy_i64_ref) { test_elemw_unary_op<int64_t>(znx_copy_i64_ref, poly_i64_copy); } -#ifdef __x86_64__ -TEST(coeffs_arithmetic, znx_neg_i64_avx) { test_elemw_unary_op<int64_t>(znx_negate_i64_avx, poly_i64_neg); } -#endif - -/// tests of the rotations out of place -template <typename T, typename F> -void test_rotation_outplace(F rotate) { - for (uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096}) { - polynomial<T> poly = polynomial<T>::random(n); - polynomial<T> expect(n); - polynomial<T> actual(n); - for (uint64_t trial = 0; trial < 10; ++trial) { - int64_t p = uniform_i64_bits(32); - // rotate by p - for (uint64_t i = 0; i < n; ++i) { - expect.set_coeff(i, poly.get_coeff(i - p)); - } - // rotate using the function - rotate(n, p, actual.data(), poly.data()); - ASSERT_EQ(actual, expect); - } - } -} - -TEST(coeffs_arithmetic, rnx_rotate_f64) { test_rotation_outplace<double>(rnx_rotate_f64); } -TEST(coeffs_arithmetic, znx_rotate_i64) { test_rotation_outplace<int64_t>(znx_rotate_i64); } - -/// tests of the rotations out of place -template <typename T, typename F> -void test_rotation_inplace(F rotate) { - for (uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096}) { - polynomial<T> poly = polynomial<T>::random(n); - polynomial<T> expect(n); - for (uint64_t trial = 0; trial < 10; ++trial) { - polynomial<T> actual = poly; - int64_t p = uniform_i64_bits(32); - // rotate by p - for (uint64_t i = 0; i < n; ++i) { - expect.set_coeff(i, poly.get_coeff(i - p)); - } - // rotate using the function - rotate(n, p, actual.data()); - ASSERT_EQ(actual, expect); - } - } -} - -TEST(coeffs_arithmetic, rnx_rotate_inplace_f64) { test_rotation_inplace<double>(rnx_rotate_inplace_f64); } - -TEST(coeffs_arithmetic, znx_rotate_inplace_i64) { test_rotation_inplace<int64_t>(znx_rotate_inplace_i64); } - -/// tests of the rotations out of place -template <typename T, typename F> -void test_mul_xp_minus_one_outplace(F rotate) { - for (uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096}) { - polynomial<T> poly = polynomial<T>::random(n); - polynomial<T> expect(n); - polynomial<T> actual(n); - for (uint64_t trial = 0; trial < 10; ++trial) { - int64_t p = uniform_i64_bits(32); - // rotate by p - for (uint64_t i = 0; i < n; ++i) { - expect.set_coeff(i, poly.get_coeff(i - p) - poly.get_coeff(i)); - } - // rotate using the function - rotate(n, p, actual.data(), poly.data()); - ASSERT_EQ(actual, expect); - } - } -} - -TEST(coeffs_arithmetic, rnx_mul_xp_minus_one_f64) { test_mul_xp_minus_one_outplace<double>(rnx_mul_xp_minus_one); } -TEST(coeffs_arithmetic, znx_mul_xp_minus_one_i64) { test_mul_xp_minus_one_outplace<int64_t>(znx_mul_xp_minus_one); } - -/// tests of the rotations out of place -template <typename T, typename F> -void test_mul_xp_minus_one_inplace(F rotate) { - for (uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096}) { - polynomial<T> poly = polynomial<T>::random(n); - polynomial<T> expect(n); - for (uint64_t trial = 0; trial < 10; ++trial) { - polynomial<T> actual = poly; - int64_t p = uniform_i64_bits(32); - // rotate by p - for (uint64_t i = 0; i < n; ++i) { - expect.set_coeff(i, poly.get_coeff(i - p) - poly.get_coeff(i)); - } - // rotate using the function - rotate(n, p, actual.data()); - ASSERT_EQ(actual, expect); - } - } -} - -TEST(coeffs_arithmetic, rnx_mul_xp_minus_one_inplace_f64) { - test_mul_xp_minus_one_inplace<double>(rnx_mul_xp_minus_one_inplace); -} - -// TEST(coeffs_arithmetic, znx_mul_xp_minus_one_inplace_i64) { -// test_mul_xp_minus_one_inplace<int64_t>(znx_rotate_inplace_i64); } -/// tests of the automorphisms out of place -template <typename T, typename F> -void test_automorphism_outplace(F automorphism) { - for (uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096}) { - polynomial<T> poly = polynomial<T>::random(n); - polynomial<T> expect(n); - polynomial<T> actual(n); - for (uint64_t trial = 0; trial < 10; ++trial) { - int64_t p = uniform_i64_bits(32) | int64_t(1); // make it odd - // automorphism p - for (uint64_t i = 0; i < n; ++i) { - expect.set_coeff(i * p, poly.get_coeff(i)); - } - // rotate using the function - automorphism(n, p, actual.data(), poly.data()); - ASSERT_EQ(actual, expect); - } - } -} - -TEST(coeffs_arithmetic, rnx_automorphism_f64) { test_automorphism_outplace<double>(rnx_automorphism_f64); } -TEST(coeffs_arithmetic, znx_automorphism_i64) { test_automorphism_outplace<int64_t>(znx_automorphism_i64); } - -/// tests of the automorphisms out of place -template <typename T, typename F> -void test_automorphism_inplace(F automorphism) { - for (uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096, 16384}) { - polynomial<T> poly = polynomial<T>::random(n); - polynomial<T> expect(n); - for (uint64_t trial = 0; trial < 20; ++trial) { - polynomial<T> actual = poly; - int64_t p = uniform_i64_bits(32) | int64_t(1); // make it odd - // automorphism p - for (uint64_t i = 0; i < n; ++i) { - expect.set_coeff(i * p, poly.get_coeff(i)); - } - automorphism(n, p, actual.data()); - if (!(actual == expect)) { - std::cerr << "automorphism p: " << p << std::endl; - for (uint64_t i = 0; i < n; ++i) { - std::cerr << i << " " << actual.get_coeff(i) << " vs " << expect.get_coeff(i) << " " - << (actual.get_coeff(i) == expect.get_coeff(i)) << std::endl; - } - } - ASSERT_EQ(actual, expect); - } - } -} -TEST(coeffs_arithmetic, rnx_automorphism_inplace_f64) { - test_automorphism_inplace<double>(rnx_automorphism_inplace_f64); -} -TEST(coeffs_arithmetic, znx_automorphism_inplace_i64) { - test_automorphism_inplace<int64_t>(znx_automorphism_inplace_i64); -} - -// TODO: write a test later! - -/** - * @brief res = (X^p-1).in - * @param nn the ring dimension - * @param p must be between -2nn <= p <= 2nn - * @param in is a rnx/znx vector of dimension nn - */ -EXPORT void rnx_mul_xp_minus_one(uint64_t nn, int64_t p, double* res, const double* in); -EXPORT void znx_mul_xp_minus_one(uint64_t nn, int64_t p, int64_t* res, const int64_t* in); - -// normalize with no carry in nor carry out -template <uint8_t inplace_flag, typename F> -void test_znx_normalize(F normalize) { - for (const uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096}) { - polynomial<int64_t> inp = znx_i64::random_log2bound(n, 62); - if (n >= 2) { - inp.set_coeff(0, -(INT64_C(1) << 62)); - inp.set_coeff(1, (INT64_C(1) << 62)); - } - for (const uint64_t base_k : {2, 3, 19, 35, 62}) { - polynomial<int64_t> out; - int64_t* inp_ptr; - if (inplace_flag == 1) { - out = polynomial<int64_t>(inp); - inp_ptr = out.data(); - } else { - out = polynomial<int64_t>(n); - inp_ptr = inp.data(); - } - - znx_normalize(n, base_k, out.data(), nullptr, inp_ptr, nullptr); - for (uint64_t i = 0; i < n; ++i) { - const int64_t x = inp.get_coeff(i); - const int64_t y = out.get_coeff(i); - const int64_t y_exp = centermod(x, INT64_C(1) << base_k); - ASSERT_EQ(y, y_exp) << n << " " << base_k << " " << i << " " << x << " " << y; - } - } - } -} - -TEST(coeffs_arithmetic, znx_normalize_outplace) { test_znx_normalize<0>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_inplace) { test_znx_normalize<1>(znx_normalize); } - -// normalize with no carry in nor carry out -template <uint8_t inplace_flag, bool has_output, typename F> -void test_znx_normalize_cout(F normalize) { - static_assert(inplace_flag < 3, "either out or cout can be inplace with inp"); - for (const uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096}) { - polynomial<int64_t> inp = znx_i64::random_log2bound(n, 62); - if (n >= 2) { - inp.set_coeff(0, -(INT64_C(1) << 62)); - inp.set_coeff(1, (INT64_C(1) << 62)); - } - for (const uint64_t base_k : {2, 3, 19, 35, 62}) { - polynomial<int64_t> out, cout; - int64_t* inp_ptr; - if (inplace_flag == 1) { - // out and inp are the same - out = polynomial<int64_t>(inp); - inp_ptr = out.data(); - cout = polynomial<int64_t>(n); - } else if (inplace_flag == 2) { - // carry out and inp are the same - cout = polynomial<int64_t>(inp); - inp_ptr = cout.data(); - out = polynomial<int64_t>(n); - } else { - // inp, out and carry out are distinct - out = polynomial<int64_t>(n); - cout = polynomial<int64_t>(n); - inp_ptr = inp.data(); - } - - znx_normalize(n, base_k, has_output ? out.data() : nullptr, cout.data(), inp_ptr, nullptr); - for (uint64_t i = 0; i < n; ++i) { - const int64_t x = inp.get_coeff(i); - const int64_t co = cout.get_coeff(i); - const int64_t y_exp = centermod((int64_t)x, INT64_C(1) << base_k); - const int64_t co_exp = (x - y_exp) >> base_k; - ASSERT_EQ(co, co_exp); - - if (has_output) { - const int64_t y = out.get_coeff(i); - ASSERT_EQ(y, y_exp); - } - } - } - } -} - -TEST(coeffs_arithmetic, znx_normalize_cout_outplace) { test_znx_normalize_cout<0, false>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_out_cout_outplace) { test_znx_normalize_cout<0, true>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_cout_inplace1) { test_znx_normalize_cout<1, false>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_out_cout_inplace1) { test_znx_normalize_cout<1, true>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_cout_inplace2) { test_znx_normalize_cout<2, false>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_out_cout_inplace2) { test_znx_normalize_cout<2, true>(znx_normalize); } - -// normalize with no carry in nor carry out -template <uint8_t inplace_flag, typename F> -void test_znx_normalize_cin(F normalize) { - static_assert(inplace_flag < 3, "either inp or cin can be inplace with out"); - for (const uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096}) { - polynomial<int64_t> inp = znx_i64::random_log2bound(n, 62); - if (n >= 4) { - inp.set_coeff(0, -(INT64_C(1) << 62)); - inp.set_coeff(1, -(INT64_C(1) << 62)); - inp.set_coeff(2, (INT64_C(1) << 62)); - inp.set_coeff(3, (INT64_C(1) << 62)); - } - for (const uint64_t base_k : {2, 3, 19, 35, 62}) { - polynomial<int64_t> cin = znx_i64::random_log2bound(n, 62); - if (n >= 4) { - inp.set_coeff(0, -(INT64_C(1) << 62)); - inp.set_coeff(1, (INT64_C(1) << 62)); - inp.set_coeff(0, -(INT64_C(1) << 62)); - inp.set_coeff(1, (INT64_C(1) << 62)); - } - - polynomial<int64_t> out; - int64_t *inp_ptr, *cin_ptr; - if (inplace_flag == 1) { - // out and inp are the same - out = polynomial<int64_t>(inp); - inp_ptr = out.data(); - cin_ptr = cin.data(); - } else if (inplace_flag == 2) { - // out and carry in are the same - out = polynomial<int64_t>(cin); - inp_ptr = inp.data(); - cin_ptr = out.data(); - } else { - // inp, carry in and out are distinct - out = polynomial<int64_t>(n); - inp_ptr = inp.data(); - cin_ptr = cin.data(); - } - - znx_normalize(n, base_k, out.data(), nullptr, inp_ptr, cin_ptr); - for (uint64_t i = 0; i < n; ++i) { - const int64_t x = inp.get_coeff(i); - const int64_t ci = cin.get_coeff(i); - const int64_t y = out.get_coeff(i); - - const __int128_t xp = (__int128_t)x + ci; - const int64_t y_exp = centermod((int64_t)xp, INT64_C(1) << base_k); - - ASSERT_EQ(y, y_exp) << n << " " << base_k << " " << i << " " << x << " " << y << " " << ci; - } - } - } -} - -TEST(coeffs_arithmetic, znx_normalize_cin_outplace) { test_znx_normalize_cin<0>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_cin_inplace1) { test_znx_normalize_cin<1>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_cin_inplace2) { test_znx_normalize_cin<2>(znx_normalize); } - -// normalize with no carry in nor carry out -template <uint8_t inplace_flag, bool has_output, typename F> -void test_znx_normalize_cin_cout(F normalize) { - static_assert(inplace_flag < 7, "either inp or cin can be inplace with out"); - for (const uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096}) { - polynomial<int64_t> inp = znx_i64::random_log2bound(n, 62); - if (n >= 4) { - inp.set_coeff(0, -(INT64_C(1) << 62)); - inp.set_coeff(1, -(INT64_C(1) << 62)); - inp.set_coeff(2, (INT64_C(1) << 62)); - inp.set_coeff(3, (INT64_C(1) << 62)); - } - for (const uint64_t base_k : {2, 3, 19, 35, 62}) { - polynomial<int64_t> cin = znx_i64::random_log2bound(n, 62); - if (n >= 4) { - inp.set_coeff(0, -(INT64_C(1) << 62)); - inp.set_coeff(1, (INT64_C(1) << 62)); - inp.set_coeff(0, -(INT64_C(1) << 62)); - inp.set_coeff(1, (INT64_C(1) << 62)); - } - - polynomial<int64_t> out, cout; - int64_t *inp_ptr, *cin_ptr; - if (inplace_flag == 1) { - // out == inp - out = polynomial<int64_t>(inp); - cout = polynomial<int64_t>(n); - inp_ptr = out.data(); - cin_ptr = cin.data(); - } else if (inplace_flag == 2) { - // cout == inp - out = polynomial<int64_t>(n); - cout = polynomial<int64_t>(inp); - inp_ptr = cout.data(); - cin_ptr = cin.data(); - } else if (inplace_flag == 3) { - // out == cin - out = polynomial<int64_t>(cin); - cout = polynomial<int64_t>(n); - inp_ptr = inp.data(); - cin_ptr = out.data(); - } else if (inplace_flag == 4) { - // cout == cin - out = polynomial<int64_t>(n); - cout = polynomial<int64_t>(cin); - inp_ptr = inp.data(); - cin_ptr = cout.data(); - } else if (inplace_flag == 5) { - // out == inp, cout == cin - out = polynomial<int64_t>(inp); - cout = polynomial<int64_t>(cin); - inp_ptr = out.data(); - cin_ptr = cout.data(); - } else if (inplace_flag == 6) { - // out == cin, cout == inp - out = polynomial<int64_t>(cin); - cout = polynomial<int64_t>(inp); - inp_ptr = cout.data(); - cin_ptr = out.data(); - } else { - out = polynomial<int64_t>(n); - cout = polynomial<int64_t>(n); - inp_ptr = inp.data(); - cin_ptr = cin.data(); - } - - znx_normalize(n, base_k, has_output ? out.data() : nullptr, cout.data(), inp_ptr, cin_ptr); - for (uint64_t i = 0; i < n; ++i) { - const int64_t x = inp.get_coeff(i); - const int64_t ci = cin.get_coeff(i); - const int64_t co = cout.get_coeff(i); - - const __int128_t xp = (__int128_t)x + ci; - const int64_t y_exp = centermod((int64_t)xp, INT64_C(1) << base_k); - const int64_t co_exp = (xp - y_exp) >> base_k; - ASSERT_EQ(co, co_exp); - - if (has_output) { - const int64_t y = out.get_coeff(i); - ASSERT_EQ(y, y_exp); - } - } - } - } -} - -TEST(coeffs_arithmetic, znx_normalize_cin_cout_outplace) { test_znx_normalize_cin_cout<0, false>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_out_cin_cout_outplace) { test_znx_normalize_cin_cout<0, true>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_cin_cout_inplace1) { test_znx_normalize_cin_cout<1, false>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_out_cin_cout_inplace1) { test_znx_normalize_cin_cout<1, true>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_cin_cout_inplace2) { test_znx_normalize_cin_cout<2, false>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_out_cin_cout_inplace2) { test_znx_normalize_cin_cout<2, true>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_cin_cout_inplace3) { test_znx_normalize_cin_cout<3, false>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_out_cin_cout_inplace3) { test_znx_normalize_cin_cout<3, true>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_cin_cout_inplace4) { test_znx_normalize_cin_cout<4, false>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_out_cin_cout_inplace4) { test_znx_normalize_cin_cout<4, true>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_cin_cout_inplace5) { test_znx_normalize_cin_cout<5, false>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_out_cin_cout_inplace5) { test_znx_normalize_cin_cout<5, true>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_cin_cout_inplace6) { test_znx_normalize_cin_cout<6, false>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_out_cin_cout_inplace6) { test_znx_normalize_cin_cout<6, true>(znx_normalize); }
D spqlios/lib/test/spqlios_cplx_conversions_test.cpp

@@ -1,86 +0,0 @@

-#include <gtest/gtest.h> - -#include <cmath> - -#include "spqlios/cplx/cplx_fft_internal.h" -#include "spqlios/cplx/cplx_fft_private.h" - -#ifdef __x86_64__ -TEST(fft, cplx_from_znx32_ref_vs_fma) { - const uint32_t m = 128; - int32_t* src = (int32_t*)spqlios_alloc_custom_align(32, 10 * m * sizeof(int32_t)); - CPLX* dst1 = (CPLX*)(src + 2 * m); - CPLX* dst2 = (CPLX*)(src + 6 * m); - for (uint64_t i = 0; i < 2 * m; ++i) { - src[i] = rand() - RAND_MAX / 2; - } - CPLX_FROM_ZNX32_PRECOMP precomp; - precomp.m = m; - cplx_from_znx32_ref(&precomp, dst1, src); - // cplx_from_znx32_simple(m, 32, dst1, src); - cplx_from_znx32_avx2_fma(&precomp, dst2, src); - for (uint64_t i = 0; i < m; ++i) { - ASSERT_EQ(dst1[i][0], dst2[i][0]); - ASSERT_EQ(dst1[i][1], dst2[i][1]); - } - spqlios_free(src); -} -#endif - -#ifdef __x86_64__ -TEST(fft, cplx_from_tnx32_ref_vs_fma) { - const uint32_t m = 128; - int32_t* src = (int32_t*)spqlios_alloc_custom_align(32, 10 * m * sizeof(int32_t)); - CPLX* dst1 = (CPLX*)(src + 2 * m); - CPLX* dst2 = (CPLX*)(src + 6 * m); - for (uint64_t i = 0; i < 2 * m; ++i) { - src[i] = rand() + (rand() << 20); - } - CPLX_FROM_TNX32_PRECOMP precomp; - precomp.m = m; - cplx_from_tnx32_ref(&precomp, dst1, src); - // cplx_from_tnx32_simple(m, dst1, src); - cplx_from_tnx32_avx2_fma(&precomp, dst2, src); - for (uint64_t i = 0; i < m; ++i) { - ASSERT_EQ(dst1[i][0], dst2[i][0]); - ASSERT_EQ(dst1[i][1], dst2[i][1]); - } - spqlios_free(src); -} -#endif - -#ifdef __x86_64__ -TEST(fft, cplx_to_tnx32_ref_vs_fma) { - for (const uint32_t m : {8, 128, 1024, 65536}) { - for (const double divisor : {double(1), double(m), double(0.5)}) { - CPLX* src = (CPLX*)spqlios_alloc_custom_align(32, 10 * m * sizeof(int32_t)); - int32_t* dst1 = (int32_t*)(src + m); - int32_t* dst2 = (int32_t*)(src + 2 * m); - for (uint64_t i = 0; i < 2 * m; ++i) { - src[i][0] = (rand() / double(RAND_MAX) - 0.5) * pow(2., 19 - (rand() % 60)) * divisor; - src[i][1] = (rand() / double(RAND_MAX) - 0.5) * pow(2., 19 - (rand() % 60)) * divisor; - } - CPLX_TO_TNX32_PRECOMP precomp; - precomp.m = m; - precomp.divisor = divisor; - cplx_to_tnx32_ref(&precomp, dst1, src); - cplx_to_tnx32_avx2_fma(&precomp, dst2, src); - // cplx_to_tnx32_simple(m, divisor, 18, dst2, src); - for (uint64_t i = 0; i < 2 * m; ++i) { - double truevalue = - (src[i % m][i / m] / divisor - floor(src[i % m][i / m] / divisor + 0.5)) * (INT64_C(1) << 32); - if (fabs(truevalue - floor(truevalue)) == 0.5) { - // ties can differ by 0, 1 or -1 - ASSERT_LE(abs(dst1[i] - dst2[i]), 0) - << i << " " << dst1[i] << " " << dst2[i] << " " << truevalue << std::endl; - } else { - // otherwise, we should have equality - ASSERT_LE(abs(dst1[i] - dst2[i]), 0) - << i << " " << dst1[i] << " " << dst2[i] << " " << truevalue << std::endl; - } - } - spqlios_free(src); - } - } -} -#endif
D spqlios/lib/test/spqlios_cplx_fft_bench.cpp

@@ -1,112 +0,0 @@

-#include <benchmark/benchmark.h> -#include <stdint.h> - -#include <cassert> -#include <cmath> -#include <complex> -#include <cstdlib> -#include <cstring> -#include <iostream> -#include <string> -#include <vector> - -#include "../spqlios/cplx/cplx_fft_internal.h" -#include "spqlios/reim/reim_fft.h" - -using namespace std; - -void init_random_values(uint64_t n, double* v) { - for (uint64_t i = 0; i < n; ++i) v[i] = rand() - (RAND_MAX >> 1); -} - -void benchmark_cplx_fft(benchmark::State& state) { - const int32_t nn = state.range(0); - CPLX_FFT_PRECOMP* a = new_cplx_fft_precomp(nn / 2, 1); - double* c = (double*)cplx_fft_precomp_get_buffer(a, 0); - init_random_values(nn, c); - for (auto _ : state) { - // cplx_fft_simple(nn/2, c); - cplx_fft(a, c); - } - delete_cplx_fft_precomp(a); -} - -void benchmark_cplx_ifft(benchmark::State& state) { - const int32_t nn = state.range(0); - CPLX_IFFT_PRECOMP* a = new_cplx_ifft_precomp(nn / 2, 1); - double* c = (double*)cplx_ifft_precomp_get_buffer(a, 0); - init_random_values(nn, c); - for (auto _ : state) { - // cplx_ifft_simple(nn/2, c); - cplx_ifft(a, c); - } - delete_cplx_ifft_precomp(a); -} - -void benchmark_reim_fft(benchmark::State& state) { - const int32_t nn = state.range(0); - const uint32_t m = nn / 2; - REIM_FFT_PRECOMP* a = new_reim_fft_precomp(m, 1); - double* c = reim_fft_precomp_get_buffer(a, 0); - init_random_values(nn, c); - for (auto _ : state) { - // cplx_fft_simple(nn/2, c); - reim_fft(a, c); - } - delete_reim_fft_precomp(a); -} - -#ifdef __aarch64__ -EXPORT REIM_FFT_PRECOMP* new_reim_fft_precomp_neon(uint32_t m, uint32_t num_buffers); -EXPORT void reim_fft_neon(const REIM_FFT_PRECOMP* precomp, double* d); - -void benchmark_reim_fft_neon(benchmark::State& state) { - const int32_t nn = state.range(0); - const uint32_t m = nn / 2; - REIM_FFT_PRECOMP* a = new_reim_fft_precomp_neon(m, 1); - double* c = reim_fft_precomp_get_buffer(a, 0); - init_random_values(nn, c); - for (auto _ : state) { - // cplx_fft_simple(nn/2, c); - reim_fft_neon(a, c); - } - delete_reim_fft_precomp(a); -} -#endif - -void benchmark_reim_ifft(benchmark::State& state) { - const int32_t nn = state.range(0); - const uint32_t m = nn / 2; - REIM_IFFT_PRECOMP* a = new_reim_ifft_precomp(m, 1); - double* c = reim_ifft_precomp_get_buffer(a, 0); - init_random_values(nn, c); - for (auto _ : state) { - // cplx_ifft_simple(nn/2, c); - reim_ifft(a, c); - } - delete_reim_ifft_precomp(a); -} - -// #define ARGS Arg(1024)->Arg(8192)->Arg(32768)->Arg(65536) -#define ARGS Arg(64)->Arg(256)->Arg(1024)->Arg(2048)->Arg(4096)->Arg(8192)->Arg(16384)->Arg(32768)->Arg(65536) - -int main(int argc, char** argv) { - ::benchmark::Initialize(&argc, argv); - if (::benchmark::ReportUnrecognizedArguments(argc, argv)) return 1; - std::cout << "Dimensions n in the benchmark below are in \"real FFT\" modulo X^n+1" << std::endl; - std::cout << "The complex dimension m (modulo X^m-i) is half of it" << std::endl; - BENCHMARK(benchmark_cplx_fft)->ARGS; - BENCHMARK(benchmark_cplx_ifft)->ARGS; - BENCHMARK(benchmark_reim_fft)->ARGS; -#ifdef __aarch64__ - BENCHMARK(benchmark_reim_fft_neon)->ARGS; -#endif - BENCHMARK(benchmark_reim_ifft)->ARGS; - // if (CPU_SUPPORTS("avx512f")) { - // BENCHMARK(bench_cplx_fftvec_twiddle_avx512)->ARGS; - // BENCHMARK(bench_cplx_fftvec_bitwiddle_avx512)->ARGS; - //} - ::benchmark::RunSpecifiedBenchmarks(); - ::benchmark::Shutdown(); - return 0; -}
D spqlios/lib/test/spqlios_cplx_test.cpp

@@ -1,496 +0,0 @@

-#include <cmath> - -#include "gtest/gtest.h" -#include "spqlios/commons_private.h" -#include "spqlios/cplx/cplx_fft.h" -#include "spqlios/cplx/cplx_fft_internal.h" -#include "spqlios/cplx/cplx_fft_private.h" - -#ifdef __x86_64__ -TEST(fft, ifft16_fma_vs_ref) { - CPLX data[16]; - CPLX omega[8]; - for (uint64_t i = 0; i < 32; ++i) ((double*)data)[i] = 2 * i + 1; //(rand()%100)-50; - for (uint64_t i = 0; i < 16; ++i) ((double*)omega)[i] = i + 1; //(rand()%100)-50; - CPLX copydata[16]; - CPLX copyomega[8]; - memcpy(copydata, data, sizeof(copydata)); - memcpy(copyomega, omega, sizeof(copyomega)); - cplx_ifft16_avx_fma(data, omega); - cplx_ifft16_ref(copydata, copyomega); - double distance = 0; - for (uint64_t i = 0; i < 16; ++i) { - double d1 = fabs(data[i][0] - copydata[i][0]); - double d2 = fabs(data[i][0] - copydata[i][0]); - if (d1 > distance) distance = d1; - if (d2 > distance) distance = d2; - } - /* - printf("data:\n"); - for (uint64_t i=0; i<4; ++i) { - for (uint64_t j=0; j<8; ++j) { - printf("%.5lf ", data[4 * i + j / 2][j % 2]); - } - printf("\n"); - } - printf("copydata:\n"); - for (uint64_t i=0; i<4; ++i) { - for (uint64_t j=0; j<8; ++j) { - printf("%5.5lf ", copydata[4 * i + j / 2][j % 2]); - } - printf("\n"); - } - */ - ASSERT_EQ(distance, 0); -} - -#endif - -void cplx_zero(CPLX r) { r[0] = r[1] = 0; } -void cplx_addmul(CPLX r, const CPLX a, const CPLX b) { - double re = r[0] + a[0] * b[0] - a[1] * b[1]; - double im = r[1] + a[0] * b[1] + a[1] * b[0]; - r[0] = re; - r[1] = im; -} - -void halfcfft_eval(CPLX res, uint32_t nn, uint32_t k, const CPLX* coeffs, const CPLX* powomegas) { - const uint32_t N = nn / 2; - cplx_zero(res); - for (uint64_t i = 0; i < N; ++i) { - cplx_addmul(res, coeffs[i], powomegas[(k * i) % (2 * nn)]); - } -} -void halfcfft_naive(uint32_t nn, CPLX* data) { - const uint32_t N = nn / 2; - CPLX* in = (CPLX*)malloc(N * sizeof(CPLX)); - CPLX* powomega = (CPLX*)malloc(2 * nn * sizeof(CPLX)); - for (uint64_t i = 0; i < (2 * nn); ++i) { - powomega[i][0] = m_accurate_cos((M_PI * i) / nn); - powomega[i][1] = m_accurate_sin((M_PI * i) / nn); - } - memcpy(in, data, N * sizeof(CPLX)); - for (uint64_t j = 0; j < N; ++j) { - uint64_t p = rint(log2(N)) + 2; - uint64_t k = revbits(p, j) + 1; - halfcfft_eval(data[j], nn, k, in, powomega); - } - free(powomega); - free(in); -} - -#ifdef __x86_64__ -TEST(fft, fft16_fma_vs_ref) { - CPLX data[16]; - CPLX omega[8]; - for (uint64_t i = 0; i < 32; ++i) ((double*)data)[i] = rand() % 1000; - for (uint64_t i = 0; i < 16; ++i) ((double*)omega)[i] = rand() % 1000; - CPLX copydata[16]; - CPLX copyomega[8]; - memcpy(copydata, data, sizeof(copydata)); - memcpy(copyomega, omega, sizeof(copyomega)); - cplx_fft16_avx_fma(data, omega); - cplx_fft16_ref(copydata, omega); - double distance = 0; - for (uint64_t i = 0; i < 16; ++i) { - double d1 = fabs(data[i][0] - copydata[i][0]); - double d2 = fabs(data[i][0] - copydata[i][0]); - if (d1 > distance) distance = d1; - if (d2 > distance) distance = d2; - } - ASSERT_EQ(distance, 0); -} -#endif - -TEST(fft, citwiddle_then_invcitwiddle) { - CPLX om; - CPLX ombar; - CPLX data[2]; - CPLX copydata[2]; - om[0] = cos(3); - om[1] = sin(3); - ombar[0] = om[0]; - ombar[1] = -om[1]; - data[0][0] = 47; - data[0][1] = 23; - data[1][0] = -12; - data[1][1] = -9; - memcpy(copydata, data, sizeof(copydata)); - citwiddle(data[0], data[1], om); - invcitwiddle(data[0], data[1], ombar); - double distance = 0; - for (uint64_t i = 0; i < 2; ++i) { - double d1 = fabs(data[i][0] - 2 * copydata[i][0]); - double d2 = fabs(data[i][1] - 2 * copydata[i][1]); - if (d1 > distance) distance = d1; - if (d2 > distance) distance = d2; - } - ASSERT_LE(distance, 1e-9); -} - -TEST(fft, ctwiddle_then_invctwiddle) { - CPLX om; - CPLX ombar; - CPLX data[2]; - CPLX copydata[2]; - om[0] = cos(3); - om[1] = sin(3); - ombar[0] = om[0]; - ombar[1] = -om[1]; - data[0][0] = 47; - data[0][1] = 23; - data[1][0] = -12; - data[1][1] = -9; - memcpy(copydata, data, sizeof(copydata)); - ctwiddle(data[0], data[1], om); - invctwiddle(data[0], data[1], ombar); - double distance = 0; - for (uint64_t i = 0; i < 2; ++i) { - double d1 = fabs(data[i][0] - 2 * copydata[i][0]); - double d2 = fabs(data[i][1] - 2 * copydata[i][1]); - if (d1 > distance) distance = d1; - if (d2 > distance) distance = d2; - } - ASSERT_LE(distance, 1e-9); -} - -TEST(fft, fft16_then_ifft16_ref) { - CPLX full_omegas[64]; - CPLX full_omegabars[64]; - for (uint64_t i = 0; i < 64; ++i) { - full_omegas[i][0] = cos(M_PI * i / 32.); - full_omegas[i][1] = sin(M_PI * i / 32.); - full_omegabars[i][0] = full_omegas[i][0]; - full_omegabars[i][1] = -full_omegas[i][1]; - } - CPLX omega[8]; - CPLX omegabar[8]; - cplx_set(omega[0], full_omegas[8]); // j - cplx_set(omega[1], full_omegas[4]); // k - cplx_set(omega[2], full_omegas[2]); // l - cplx_set(omega[3], full_omegas[10]); // lj - cplx_set(omega[4], full_omegas[1]); // n - cplx_set(omega[5], full_omegas[9]); // nj - cplx_set(omega[6], full_omegas[5]); // nk - cplx_set(omega[7], full_omegas[13]); // njk - cplx_set(omegabar[0], full_omegabars[1]); // n - cplx_set(omegabar[1], full_omegabars[9]); // nj - cplx_set(omegabar[2], full_omegabars[5]); // nk - cplx_set(omegabar[3], full_omegabars[13]); // njk - cplx_set(omegabar[4], full_omegabars[2]); // l - cplx_set(omegabar[5], full_omegabars[10]); // lj - cplx_set(omegabar[6], full_omegabars[4]); // k - cplx_set(omegabar[7], full_omegabars[8]); // j - CPLX data[16]; - CPLX copydata[16]; - for (uint64_t i = 0; i < 32; ++i) ((double*)data)[i] = rand() % 1000; - memcpy(copydata, data, sizeof(copydata)); - cplx_fft16_ref(data, omega); - cplx_ifft16_ref(data, omegabar); - double distance = 0; - for (uint64_t i = 0; i < 16; ++i) { - double d1 = fabs(data[i][0] - 16 * copydata[i][0]); - double d2 = fabs(data[i][0] - 16 * copydata[i][0]); - if (d1 > distance) distance = d1; - if (d2 > distance) distance = d2; - } - ASSERT_LE(distance, 1e-9); -} - -TEST(fft, halfcfft_ref_vs_naive) { - for (uint64_t nn : {4, 8, 16, 64, 256, 8192}) { - uint64_t m = nn / 2; - CPLX_FFT_PRECOMP* tables = new_cplx_fft_precomp(m, 0); - CPLX* a = (CPLX*)spqlios_alloc_custom_align(32, m * sizeof(CPLX)); - CPLX* a1 = (CPLX*)spqlios_alloc_custom_align(32, m * sizeof(CPLX)); - CPLX* a2 = (CPLX*)spqlios_alloc_custom_align(32, m * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < m; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - } - memcpy(a1, a, m * sizeof(CPLX)); - memcpy(a2, a, m * sizeof(CPLX)); - - halfcfft_naive(nn, a1); - cplx_fft_naive(m, 0.25, a2); - - double d = 0; - for (uint32_t i = 0; i < nn / 2; i++) { - double dre = fabs(a1[i][0] - a2[i][0]); - double dim = fabs(a1[i][1] - a2[i][1]); - if (dre > d) d = dre; - if (dim > d) d = dim; - } - ASSERT_LE(d, nn * 1e-10) << nn; - spqlios_free(a); - spqlios_free(a1); - spqlios_free(a2); - delete_cplx_fft_precomp(tables); - } -} - -#ifdef __x86_64__ -TEST(fft, halfcfft_fma_vs_ref) { - typedef void (*FFTF)(const CPLX_FFT_PRECOMP*, void* data); - for (FFTF fft : {cplx_fft_ref, cplx_fft_avx2_fma}) { - for (uint64_t nn : {8, 16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - CPLX_FFT_PRECOMP* tables = new_cplx_fft_precomp(m, 0); - CPLX* a = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - CPLX* a1 = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - CPLX* a2 = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn / 2; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - } - memcpy(a1, a, nn / 2 * sizeof(CPLX)); - memcpy(a2, a, nn / 2 * sizeof(CPLX)); - cplx_fft_naive(m, 0.25, a2); - fft(tables, a1); - double d = 0; - for (uint32_t i = 0; i < nn / 2; i++) { - double dre = fabs(a1[i][0] - a2[i][0]); - double dim = fabs(a1[i][1] - a2[i][1]); - if (dre > d) d = dre; - if (dim > d) d = dim; - } - ASSERT_LE(d, nn * 1e-10) << nn; - spqlios_free(a); - spqlios_free(a1); - spqlios_free(a2); - delete_cplx_fft_precomp(tables); - } - } -} -#endif - -TEST(fft, halfcfft_then_ifft_ref) { - for (uint64_t nn : {4, 8, 16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - CPLX_FFT_PRECOMP* tables = new_cplx_fft_precomp(m, 0); - CPLX_IFFT_PRECOMP* itables = new_cplx_ifft_precomp(m, 0); - CPLX* a = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - CPLX* a1 = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn / 2; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - } - memcpy(a1, a, nn / 2 * sizeof(CPLX)); - cplx_fft_ref(tables, a1); - cplx_ifft_ref(itables, a1); - double d = 0; - for (uint32_t i = 0; i < nn / 2; i++) { - double dre = fabs(a[i][0] - a1[i][0] / (nn / 2)); - double dim = fabs(a[i][1] - a1[i][1] / (nn / 2)); - if (dre > d) d = dre; - if (dim > d) d = dim; - } - ASSERT_LE(d, 1e-8); - spqlios_free(a); - spqlios_free(a1); - delete_cplx_fft_precomp(tables); - delete_cplx_ifft_precomp(itables); - } -} - -#ifdef __x86_64__ -TEST(fft, halfcfft_ifft_fma_vs_ref) { - for (IFFT_FUNCTION ifft : {cplx_ifft_ref, cplx_ifft_avx2_fma}) { - for (uint64_t nn : {8, 16, 32, 1024, 4096, 8192, 65536}) { - uint64_t m = nn / 2; - CPLX_IFFT_PRECOMP* itables = new_cplx_ifft_precomp(m, 0); - CPLX* a = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - CPLX* a1 = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - CPLX* a2 = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn / 2; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - } - memcpy(a1, a, nn / 2 * sizeof(CPLX)); - memcpy(a2, a, nn / 2 * sizeof(CPLX)); - cplx_ifft_naive(m, 0.25, a2); - ifft(itables, a1); - double d = 0; - for (uint32_t i = 0; i < nn / 2; i++) { - double dre = fabs(a1[i][0] - a2[i][0]); - double dim = fabs(a1[i][1] - a2[i][1]); - if (dre > d) d = dre; - if (dim > d) d = dim; - } - ASSERT_LE(d, 1e-8); - spqlios_free(a); - spqlios_free(a1); - spqlios_free(a2); - delete_cplx_ifft_precomp(itables); - } - } -} -#endif - -// test the reference and simple implementations of mul on all dimensions -TEST(fftvec, cplx_fftvec_mul_ref) { - for (uint64_t nn : {2, 4, 8, 16, 32, 1024, 4096, 8192, 65536}) { - uint64_t m = nn / 2; - CPLX_FFTVEC_MUL_PRECOMP* precomp = new_cplx_fftvec_mul_precomp(m); - CPLX* a = new CPLX[m]; - CPLX* b = new CPLX[m]; - CPLX* r0 = new CPLX[m]; - CPLX* r1 = new CPLX[m]; - CPLX* r2 = new CPLX[m]; - int64_t p = 1 << 16; - for (uint32_t i = 0; i < m; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - b[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - b[i][1] = (rand() % p) - p / 2; - r2[i][0] = r1[i][0] = r0[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - r2[i][1] = r1[i][1] = r0[i][1] = (rand() % p) - p / 2; - } - cplx_fftvec_mul_simple(m, r0, a, b); - cplx_fftvec_mul_ref(precomp, r1, a, b); - for (uint32_t i = 0; i < m; i++) { - r2[i][0] = a[i][0] * b[i][0] - a[i][1] * b[i][1]; - r2[i][1] = a[i][0] * b[i][1] + a[i][1] * b[i][0]; - ASSERT_LE(fabs(r1[i][0] - r2[i][0]) + fabs(r1[i][1] - r2[i][1]), 1e-8); - ASSERT_LE(fabs(r0[i][0] - r2[i][0]) + fabs(r0[i][1] - r2[i][1]), 1e-8); - } - delete[] a; - delete[] b; - delete[] r0; - delete[] r1; - delete[] r2; - delete_cplx_fftvec_mul_precomp(precomp); - } -} - -// test the reference and simple implementations of addmul on all dimensions -TEST(fftvec, cplx_fftvec_addmul_ref) { - for (uint64_t nn : {2, 4, 8, 16, 32, 1024, 4096, 8192, 65536}) { - uint64_t m = nn / 2; - CPLX_FFTVEC_ADDMUL_PRECOMP* precomp = new_cplx_fftvec_addmul_precomp(m); - CPLX* a = new CPLX[m]; - CPLX* b = new CPLX[m]; - CPLX* r0 = new CPLX[m]; - CPLX* r1 = new CPLX[m]; - CPLX* r2 = new CPLX[m]; - int64_t p = 1 << 16; - for (uint32_t i = 0; i < m; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - b[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - b[i][1] = (rand() % p) - p / 2; - r2[i][0] = r1[i][0] = r0[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - r2[i][1] = r1[i][1] = r0[i][1] = (rand() % p) - p / 2; - } - cplx_fftvec_addmul_simple(m, r0, a, b); - cplx_fftvec_addmul_ref(precomp, r1, a, b); - for (uint32_t i = 0; i < m; i++) { - r2[i][0] += a[i][0] * b[i][0] - a[i][1] * b[i][1]; - r2[i][1] += a[i][0] * b[i][1] + a[i][1] * b[i][0]; - ASSERT_LE(fabs(r1[i][0] - r2[i][0]) + fabs(r1[i][1] - r2[i][1]), 1e-8); - ASSERT_LE(fabs(r0[i][0] - r2[i][0]) + fabs(r0[i][1] - r2[i][1]), 1e-8); - } - delete[] a; - delete[] b; - delete[] r0; - delete[] r1; - delete[] r2; - delete_cplx_fftvec_addmul_precomp(precomp); - } -} - -// comparative tests between mul ref vs. optimized (only relevant dimensions) -TEST(fftvec, cplx_fftvec_mul_ref_vs_optim) { - struct totest { - FFTVEC_MUL_FUNCTION f; - uint64_t min_m; - totest(FFTVEC_MUL_FUNCTION f, uint64_t min_m) : f(f), min_m(min_m) {} - }; - std::vector<totest> totestset; - totestset.emplace_back(cplx_fftvec_mul, 1); -#ifdef __x86_64__ - totestset.emplace_back(cplx_fftvec_mul_fma, 8); -#endif - for (uint64_t m : {1, 2, 4, 8, 16, 1024, 4096, 8192, 65536}) { - CPLX_FFTVEC_MUL_PRECOMP* precomp = new_cplx_fftvec_mul_precomp(m); - for (const totest& t : totestset) { - if (t.min_m > m) continue; - CPLX* a = new CPLX[m]; - CPLX* b = new CPLX[m]; - CPLX* r1 = new CPLX[m]; - CPLX* r2 = new CPLX[m]; - int64_t p = 1 << 16; - for (uint32_t i = 0; i < m; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - b[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - b[i][1] = (rand() % p) - p / 2; - r2[i][0] = r1[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - r2[i][1] = r1[i][1] = (rand() % p) - p / 2; - } - t.f(precomp, r1, a, b); - cplx_fftvec_mul_ref(precomp, r2, a, b); - for (uint32_t i = 0; i < m; i++) { - double dre = fabs(r1[i][0] - r2[i][0]); - double dim = fabs(r1[i][1] - r2[i][1]); - ASSERT_LE(dre, 1e-8); - ASSERT_LE(dim, 1e-8); - } - delete[] a; - delete[] b; - delete[] r1; - delete[] r2; - } - delete_cplx_fftvec_mul_precomp(precomp); - } -} - -// comparative tests between addmul ref vs. optimized (only relevant dimensions) -TEST(fftvec, cplx_fftvec_addmul_ref_vs_optim) { - struct totest { - FFTVEC_ADDMUL_FUNCTION f; - uint64_t min_m; - totest(FFTVEC_ADDMUL_FUNCTION f, uint64_t min_m) : f(f), min_m(min_m) {} - }; - std::vector<totest> totestset; - totestset.emplace_back(cplx_fftvec_addmul, 1); -#ifdef __x86_64__ - totestset.emplace_back(cplx_fftvec_addmul_fma, 8); -#endif - for (uint64_t m : {1, 2, 4, 8, 16, 1024, 4096, 8192, 65536}) { - CPLX_FFTVEC_ADDMUL_PRECOMP* precomp = new_cplx_fftvec_addmul_precomp(m); - for (const totest& t : totestset) { - if (t.min_m > m) continue; - CPLX* a = new CPLX[m]; - CPLX* b = new CPLX[m]; - CPLX* r1 = new CPLX[m]; - CPLX* r2 = new CPLX[m]; - int64_t p = 1 << 16; - for (uint32_t i = 0; i < m; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - b[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - b[i][1] = (rand() % p) - p / 2; - r2[i][0] = r1[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - r2[i][1] = r1[i][1] = (rand() % p) - p / 2; - } - t.f(precomp, r1, a, b); - cplx_fftvec_addmul_ref(precomp, r2, a, b); - for (uint32_t i = 0; i < m; i++) { - double dre = fabs(r1[i][0] - r2[i][0]); - double dim = fabs(r1[i][1] - r2[i][1]); - ASSERT_LE(dre, 1e-8); - ASSERT_LE(dim, 1e-8); - } - delete[] a; - delete[] b; - delete[] r1; - delete[] r2; - } - delete_cplx_fftvec_addmul_precomp(precomp); - } -}
D spqlios/lib/test/spqlios_q120_arithmetic_bench.cpp

@@ -1,136 +0,0 @@

-#include <benchmark/benchmark.h> - -#include <cstdint> - -#include "spqlios/q120/q120_arithmetic.h" - -#define ARGS Arg(128)->Arg(4096)->Arg(10000) - -template <typeof(q120_vec_mat1col_product_baa_ref) f> -void benchmark_baa(benchmark::State& state) { - const uint64_t ell = state.range(0); - q120_mat1col_product_baa_precomp* precomp = q120_new_vec_mat1col_product_baa_precomp(); - - uint64_t* a = new uint64_t[ell * 4]; - uint64_t* b = new uint64_t[ell * 4]; - uint64_t* c = new uint64_t[4]; - for (uint64_t i = 0; i < 4 * ell; i++) { - a[i] = rand(); - b[i] = rand(); - } - for (auto _ : state) { - f(precomp, ell, (q120b*)c, (q120a*)a, (q120a*)b); - } - delete[] c; - delete[] b; - delete[] a; - q120_delete_vec_mat1col_product_baa_precomp(precomp); -} - -BENCHMARK(benchmark_baa<q120_vec_mat1col_product_baa_ref>)->Name("q120_vec_mat1col_product_baa_ref")->ARGS; -BENCHMARK(benchmark_baa<q120_vec_mat1col_product_baa_avx2>)->Name("q120_vec_mat1col_product_baa_avx2")->ARGS; - -template <typeof(q120_vec_mat1col_product_bbb_ref) f> -void benchmark_bbb(benchmark::State& state) { - const uint64_t ell = state.range(0); - q120_mat1col_product_bbb_precomp* precomp = q120_new_vec_mat1col_product_bbb_precomp(); - - uint64_t* a = new uint64_t[ell * 4]; - uint64_t* b = new uint64_t[ell * 4]; - uint64_t* c = new uint64_t[4]; - for (uint64_t i = 0; i < 4 * ell; i++) { - a[i] = rand(); - b[i] = rand(); - } - for (auto _ : state) { - f(precomp, ell, (q120b*)c, (q120b*)a, (q120b*)b); - } - delete[] c; - delete[] b; - delete[] a; - q120_delete_vec_mat1col_product_bbb_precomp(precomp); -} - -BENCHMARK(benchmark_bbb<q120_vec_mat1col_product_bbb_ref>)->Name("q120_vec_mat1col_product_bbb_ref")->ARGS; -BENCHMARK(benchmark_bbb<q120_vec_mat1col_product_bbb_avx2>)->Name("q120_vec_mat1col_product_bbb_avx2")->ARGS; - -template <typeof(q120_vec_mat1col_product_bbc_ref) f> -void benchmark_bbc(benchmark::State& state) { - const uint64_t ell = state.range(0); - q120_mat1col_product_bbc_precomp* precomp = q120_new_vec_mat1col_product_bbc_precomp(); - - uint64_t* a = new uint64_t[ell * 4]; - uint64_t* b = new uint64_t[ell * 4]; - uint64_t* c = new uint64_t[4]; - for (uint64_t i = 0; i < 4 * ell; i++) { - a[i] = rand(); - b[i] = rand(); - } - for (auto _ : state) { - f(precomp, ell, (q120b*)c, (q120b*)a, (q120c*)b); - } - delete[] c; - delete[] b; - delete[] a; - q120_delete_vec_mat1col_product_bbc_precomp(precomp); -} - -BENCHMARK(benchmark_bbc<q120_vec_mat1col_product_bbc_ref>)->Name("q120_vec_mat1col_product_bbc_ref")->ARGS; -BENCHMARK(benchmark_bbc<q120_vec_mat1col_product_bbc_avx2>)->Name("q120_vec_mat1col_product_bbc_avx2")->ARGS; - -EXPORT void q120x2_vec_mat2cols_product_bbc_avx2(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y); -EXPORT void q120x2_vec_mat1col_product_bbc_avx2(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y); - -template <typeof(q120_vec_mat1col_product_bbc_ref) f> -void benchmark_x2c2_bbc(benchmark::State& state) { - const uint64_t ell = state.range(0); - q120_mat1col_product_bbc_precomp* precomp = q120_new_vec_mat1col_product_bbc_precomp(); - - uint64_t* a = new uint64_t[ell * 8]; - uint64_t* b = new uint64_t[ell * 16]; - uint64_t* c = new uint64_t[16]; - for (uint64_t i = 0; i < 8 * ell; i++) { - a[i] = rand(); - } - for (uint64_t i = 0; i < 16 * ell; i++) { - b[i] = rand(); - } - for (auto _ : state) { - f(precomp, ell, (q120b*)c, (q120b*)a, (q120c*)b); - } - delete[] c; - delete[] b; - delete[] a; - q120_delete_vec_mat1col_product_bbc_precomp(precomp); -} - -BENCHMARK(benchmark_x2c2_bbc<q120x2_vec_mat2cols_product_bbc_avx2>)->Name("q120x2_vec_mat2col_product_bbc_avx2")->ARGS; - -template <typeof(q120_vec_mat1col_product_bbc_ref) f> -void benchmark_x2c1_bbc(benchmark::State& state) { - const uint64_t ell = state.range(0); - q120_mat1col_product_bbc_precomp* precomp = q120_new_vec_mat1col_product_bbc_precomp(); - - uint64_t* a = new uint64_t[ell * 8]; - uint64_t* b = new uint64_t[ell * 8]; - uint64_t* c = new uint64_t[8]; - for (uint64_t i = 0; i < 8 * ell; i++) { - a[i] = rand(); - } - for (uint64_t i = 0; i < 8 * ell; i++) { - b[i] = rand(); - } - for (auto _ : state) { - f(precomp, ell, (q120b*)c, (q120b*)a, (q120c*)b); - } - delete[] c; - delete[] b; - delete[] a; - q120_delete_vec_mat1col_product_bbc_precomp(precomp); -} - -BENCHMARK(benchmark_x2c1_bbc<q120x2_vec_mat1col_product_bbc_avx2>)->Name("q120x2_vec_mat1col_product_bbc_avx2")->ARGS; - -BENCHMARK_MAIN();
D spqlios/lib/test/spqlios_q120_arithmetic_test.cpp

@@ -1,437 +0,0 @@

-#include <gtest/gtest.h> - -#include <cstdint> -#include <vector> - -#include "spqlios/q120/q120_arithmetic.h" -#include "test/testlib/negacyclic_polynomial.h" -#include "test/testlib/ntt120_layouts.h" -#include "testlib/mod_q120.h" - -typedef typeof(q120_vec_mat1col_product_baa_ref) vec_mat1col_product_baa_f; - -void test_vec_mat1col_product_baa(vec_mat1col_product_baa_f vec_mat1col_product_baa) { - q120_mat1col_product_baa_precomp* precomp = q120_new_vec_mat1col_product_baa_precomp(); - for (uint64_t ell : {1, 2, 100, 10000}) { - std::vector<uint64_t> a(ell * 4); - std::vector<uint64_t> b(ell * 4); - std::vector<uint64_t> res(4); - uint64_t* pa = a.data(); - uint64_t* pb = b.data(); - uint64_t* pr = res.data(); - // generate some random data - uniform_q120b(pr); - for (uint64_t i = 0; i < ell; ++i) { - uniform_q120a(pa + 4 * i); - uniform_q120a(pb + 4 * i); - } - // compute the expected result - mod_q120 expect_r; - for (uint64_t i = 0; i < ell; ++i) { - expect_r += mod_q120::from_q120a(pa + 4 * i) * mod_q120::from_q120a(pb + 4 * i); - } - // compute the function - vec_mat1col_product_baa(precomp, ell, (q120b*)pr, (q120a*)pa, (q120a*)pb); - mod_q120 comp_r = mod_q120::from_q120b(pr); - // check for equality - ASSERT_EQ(comp_r, expect_r) << ell; - } - q120_delete_vec_mat1col_product_baa_precomp(precomp); -} - -TEST(q120_arithmetic, q120_vec_mat1col_product_baa_ref) { - test_vec_mat1col_product_baa(q120_vec_mat1col_product_baa_ref); -} -#ifdef __x86_64__ -TEST(q120_arithmetic, q120_vec_mat1col_product_baa_avx2) { - test_vec_mat1col_product_baa(q120_vec_mat1col_product_baa_avx2); -} -#endif - -typedef typeof(q120_vec_mat1col_product_bbb_ref) vec_mat1col_product_bbb_f; - -void test_vec_mat1col_product_bbb(vec_mat1col_product_bbb_f vec_mat1col_product_bbb) { - q120_mat1col_product_bbb_precomp* precomp = q120_new_vec_mat1col_product_bbb_precomp(); - for (uint64_t ell : {1, 2, 100, 10000}) { - std::vector<uint64_t> a(ell * 4); - std::vector<uint64_t> b(ell * 4); - std::vector<uint64_t> res(4); - uint64_t* pa = a.data(); - uint64_t* pb = b.data(); - uint64_t* pr = res.data(); - // generate some random data - uniform_q120b(pr); - for (uint64_t i = 0; i < ell; ++i) { - uniform_q120b(pa + 4 * i); - uniform_q120b(pb + 4 * i); - } - // compute the expected result - mod_q120 expect_r; - for (uint64_t i = 0; i < ell; ++i) { - expect_r += mod_q120::from_q120b(pa + 4 * i) * mod_q120::from_q120b(pb + 4 * i); - } - // compute the function - vec_mat1col_product_bbb(precomp, ell, (q120b*)pr, (q120b*)pa, (q120b*)pb); - mod_q120 comp_r = mod_q120::from_q120b(pr); - // check for equality - ASSERT_EQ(comp_r, expect_r); - } - q120_delete_vec_mat1col_product_bbb_precomp(precomp); -} - -TEST(q120_arithmetic, q120_vec_mat1col_product_bbb_ref) { - test_vec_mat1col_product_bbb(q120_vec_mat1col_product_bbb_ref); -} -#ifdef __x86_64__ -TEST(q120_arithmetic, q120_vec_mat1col_product_bbb_avx2) { - test_vec_mat1col_product_bbb(q120_vec_mat1col_product_bbb_avx2); -} -#endif - -typedef typeof(q120_vec_mat1col_product_bbc_ref) vec_mat1col_product_bbc_f; - -void test_vec_mat1col_product_bbc(vec_mat1col_product_bbc_f vec_mat1col_product_bbc) { - q120_mat1col_product_bbc_precomp* precomp = q120_new_vec_mat1col_product_bbc_precomp(); - for (uint64_t ell : {1, 2, 100, 10000}) { - std::vector<uint64_t> a(ell * 4); - std::vector<uint64_t> b(ell * 4); - std::vector<uint64_t> res(4); - uint64_t* pa = a.data(); - uint64_t* pb = b.data(); - uint64_t* pr = res.data(); - // generate some random data - uniform_q120b(pr); - for (uint64_t i = 0; i < ell; ++i) { - uniform_q120b(pa + 4 * i); - uniform_q120c(pb + 4 * i); - } - // compute the expected result - mod_q120 expect_r; - for (uint64_t i = 0; i < ell; ++i) { - expect_r += mod_q120::from_q120b(pa + 4 * i) * mod_q120::from_q120c(pb + 4 * i); - } - // compute the function - vec_mat1col_product_bbc(precomp, ell, (q120b*)pr, (q120b*)pa, (q120c*)pb); - mod_q120 comp_r = mod_q120::from_q120b(pr); - // check for equality - ASSERT_EQ(comp_r, expect_r); - } - q120_delete_vec_mat1col_product_bbc_precomp(precomp); -} - -TEST(q120_arithmetic, q120_vec_mat1col_product_bbc_ref) { - test_vec_mat1col_product_bbc(q120_vec_mat1col_product_bbc_ref); -} -#ifdef __x86_64__ -TEST(q120_arithmetic, q120_vec_mat1col_product_bbc_avx2) { - test_vec_mat1col_product_bbc(q120_vec_mat1col_product_bbc_avx2); -} -#endif - -EXPORT void q120x2_vec_mat2cols_product_bbc_avx2(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y); -EXPORT void q120x2_vec_mat1col_product_bbc_avx2(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y); - -typedef typeof(q120x2_vec_mat2cols_product_bbc_avx2) q120x2_prod_bbc_f; - -void test_q120x2_vec_mat2cols_product_bbc(q120x2_prod_bbc_f q120x2_prod_bbc) { - q120_mat1col_product_bbc_precomp* precomp = q120_new_vec_mat1col_product_bbc_precomp(); - for (uint64_t ell : {1, 2, 100, 10000}) { - std::vector<uint64_t> a(ell * 8); - std::vector<uint64_t> b(ell * 16); - std::vector<uint64_t> res(16); - uint64_t* pa = a.data(); - uint64_t* pb = b.data(); - uint64_t* pr = res.data(); - // generate some random data - uniform_q120b(pr); - for (uint64_t i = 0; i < 2 * ell; ++i) { - uniform_q120b(pa + 4 * i); - } - for (uint64_t i = 0; i < 4 * ell; ++i) { - uniform_q120c(pb + 4 * i); - } - // compute the expected result - mod_q120 expect_r[4]; - for (uint64_t i = 0; i < ell; ++i) { - mod_q120 va = mod_q120::from_q120b(pa + 8 * i); - mod_q120 vb = mod_q120::from_q120b(pa + 8 * i + 4); - mod_q120 m1a = mod_q120::from_q120c(pb + 16 * i); - mod_q120 m1b = mod_q120::from_q120c(pb + 16 * i + 4); - mod_q120 m2a = mod_q120::from_q120c(pb + 16 * i + 8); - mod_q120 m2b = mod_q120::from_q120c(pb + 16 * i + 12); - expect_r[0] += va * m1a; - expect_r[1] += vb * m1b; - expect_r[2] += va * m2a; - expect_r[3] += vb * m2b; - } - // compute the function - q120x2_prod_bbc(precomp, ell, (q120b*)pr, (q120b*)pa, (q120c*)pb); - // check for equality - ASSERT_EQ(mod_q120::from_q120b(pr), expect_r[0]); - ASSERT_EQ(mod_q120::from_q120b(pr + 4), expect_r[1]); - ASSERT_EQ(mod_q120::from_q120b(pr + 8), expect_r[2]); - ASSERT_EQ(mod_q120::from_q120b(pr + 12), expect_r[3]); - } - q120_delete_vec_mat1col_product_bbc_precomp(precomp); -} - -TEST(q120_arithmetic, q120x2_vec_mat2cols_product_bbc_ref) { - test_q120x2_vec_mat2cols_product_bbc(q120x2_vec_mat2cols_product_bbc_ref); -} -#ifdef __x86_64__ -TEST(q120_arithmetic, q120x2_vec_mat2cols_product_bbc_avx2) { - test_q120x2_vec_mat2cols_product_bbc(q120x2_vec_mat2cols_product_bbc_avx2); -} -#endif - -typedef typeof(q120x2_vec_mat1col_product_bbc_avx2) q120x2_c1_prod_bbc_f; - -void test_q120x2_vec_mat1col_product_bbc(q120x2_c1_prod_bbc_f q120x2_c1_prod_bbc) { - q120_mat1col_product_bbc_precomp* precomp = q120_new_vec_mat1col_product_bbc_precomp(); - for (uint64_t ell : {1, 2, 100, 10000}) { - std::vector<uint64_t> a(ell * 8); - std::vector<uint64_t> b(ell * 8); - std::vector<uint64_t> res(8); - uint64_t* pa = a.data(); - uint64_t* pb = b.data(); - uint64_t* pr = res.data(); - // generate some random data - uniform_q120b(pr); - for (uint64_t i = 0; i < 2 * ell; ++i) { - uniform_q120b(pa + 4 * i); - } - for (uint64_t i = 0; i < 2 * ell; ++i) { - uniform_q120c(pb + 4 * i); - } - // compute the expected result - mod_q120 expect_r[2]; - for (uint64_t i = 0; i < ell; ++i) { - mod_q120 va = mod_q120::from_q120b(pa + 8 * i); - mod_q120 vb = mod_q120::from_q120b(pa + 8 * i + 4); - mod_q120 m1a = mod_q120::from_q120c(pb + 8 * i); - mod_q120 m1b = mod_q120::from_q120c(pb + 8 * i + 4); - expect_r[0] += va * m1a; - expect_r[1] += vb * m1b; - } - // compute the function - q120x2_c1_prod_bbc(precomp, ell, (q120b*)pr, (q120b*)pa, (q120c*)pb); - // check for equality - ASSERT_EQ(mod_q120::from_q120b(pr), expect_r[0]); - ASSERT_EQ(mod_q120::from_q120b(pr + 4), expect_r[1]); - } - q120_delete_vec_mat1col_product_bbc_precomp(precomp); -} - -TEST(q120_arithmetic, q120x2_vec_mat1col_product_bbc_ref) { - test_q120x2_vec_mat1col_product_bbc(q120x2_vec_mat1col_product_bbc_ref); -} -#ifdef __x86_64__ -TEST(q120_arithmetic, q120x2_vec_mat1col_product_bbc_avx2) { - test_q120x2_vec_mat1col_product_bbc(q120x2_vec_mat1col_product_bbc_avx2); -} -#endif - -typedef typeof(q120x2_extract_1blk_from_q120b_ref) q120x2_extract_f; -void test_q120x2_extract_1blk(q120x2_extract_f q120x2_extract) { - for (uint64_t n : {2, 4, 64}) { - ntt120_vec_znx_dft_layout v(n, 1); - std::vector<uint64_t> r(8); - std::vector<uint64_t> expect(8); - for (uint64_t blk = 0; blk < n / 2; ++blk) { - for (uint64_t i = 0; i < 8; ++i) { - expect[i] = uniform_u64(); - } - memcpy(v.get_blk(0, blk), expect.data(), 8 * sizeof(uint64_t)); - q120x2_extract_1blk_from_q120b_ref(n, blk, (q120x2b*)r.data(), (q120b*)v.data); - ASSERT_EQ(r, expect); - } - } -} - -TEST(q120_arithmetic, q120x2_extract_1blk_from_q120b_ref) { - test_q120x2_extract_1blk(q120x2_extract_1blk_from_q120b_ref); -} - -typedef typeof(q120x2_extract_1blk_from_contiguous_q120b_ref) q120x2_extract_vec_f; -void test_q120x2_extract_1blk_vec(q120x2_extract_vec_f q120x2_extract) { - for (uint64_t n : {2, 4, 32}) { - for (uint64_t size : {1, 2, 7}) { - ntt120_vec_znx_dft_layout v(n, size); - std::vector<uint64_t> r(8 * size); - std::vector<uint64_t> expect(8 * size); - for (uint64_t blk = 0; blk < n / 2; ++blk) { - for (uint64_t i = 0; i < 8 * size; ++i) { - expect[i] = uniform_u64(); - } - for (uint64_t i = 0; i < size; ++i) { - memcpy(v.get_blk(i, blk), expect.data() + 8 * i, 8 * sizeof(uint64_t)); - } - q120x2_extract(n, size, blk, (q120x2b*)r.data(), (q120b*)v.data); - ASSERT_EQ(r, expect); - } - } - } -} - -TEST(q120_arithmetic, q120x2_extract_1blk_from_contiguous_q120b_ref) { - test_q120x2_extract_1blk_vec(q120x2_extract_1blk_from_contiguous_q120b_ref); -} - -typedef typeof(q120x2b_save_1blk_to_q120b_ref) q120x2_save_f; -void test_q120x2_save_1blk(q120x2_save_f q120x2_save) { - for (uint64_t n : {2, 4, 64}) { - ntt120_vec_znx_dft_layout v(n, 1); - std::vector<uint64_t> r(8); - std::vector<uint64_t> expect(8); - for (uint64_t blk = 0; blk < n / 2; ++blk) { - for (uint64_t i = 0; i < 8; ++i) { - expect[i] = uniform_u64(); - } - q120x2_save(n, blk, (q120b*)v.data, (q120x2b*)expect.data()); - memcpy(r.data(), v.get_blk(0, blk), 8 * sizeof(uint64_t)); - ASSERT_EQ(r, expect); - } - } -} - -TEST(q120_arithmetic, q120x2b_save_1blk_to_q120b_ref) { test_q120x2_save_1blk(q120x2b_save_1blk_to_q120b_ref); } - -TEST(q120_arithmetic, q120_add_bbb_simple) { - for (const uint64_t n : {2, 4, 1024}) { - std::vector<uint64_t> a(n * 4); - std::vector<uint64_t> b(n * 4); - std::vector<uint64_t> r(n * 4); - uint64_t* pa = a.data(); - uint64_t* pb = b.data(); - uint64_t* pr = r.data(); - - // generate some random data - for (uint64_t i = 0; i < n; ++i) { - uniform_q120b(pa + 4 * i); - uniform_q120b(pb + 4 * i); - } - - // compute the function - q120_add_bbb_simple(n, (q120b*)pr, (q120b*)pa, (q120b*)pb); - - for (uint64_t i = 0; i < n; ++i) { - mod_q120 ae = mod_q120::from_q120b(pa + 4 * i); - mod_q120 be = mod_q120::from_q120b(pb + 4 * i); - mod_q120 re = mod_q120::from_q120b(pr + 4 * i); - - ASSERT_EQ(ae + be, re); - } - } -} - -TEST(q120_arithmetic, q120_add_ccc_simple) { - for (const uint64_t n : {2, 4, 1024}) { - std::vector<uint64_t> a(n * 4); - std::vector<uint64_t> b(n * 4); - std::vector<uint64_t> r(n * 4); - uint64_t* pa = a.data(); - uint64_t* pb = b.data(); - uint64_t* pr = r.data(); - - // generate some random data - for (uint64_t i = 0; i < n; ++i) { - uniform_q120c(pa + 4 * i); - uniform_q120c(pb + 4 * i); - } - - // compute the function - q120_add_ccc_simple(n, (q120c*)pr, (q120c*)pa, (q120c*)pb); - - for (uint64_t i = 0; i < n; ++i) { - mod_q120 ae = mod_q120::from_q120c(pa + 4 * i); - mod_q120 be = mod_q120::from_q120c(pb + 4 * i); - mod_q120 re = mod_q120::from_q120c(pr + 4 * i); - - ASSERT_EQ(ae + be, re); - } - } -} - -TEST(q120_arithmetic, q120_c_from_b_simple) { - for (const uint64_t n : {2, 4, 1024}) { - std::vector<uint64_t> a(n * 4); - std::vector<uint64_t> r(n * 4); - uint64_t* pa = a.data(); - uint64_t* pr = r.data(); - - // generate some random data - for (uint64_t i = 0; i < n; ++i) { - uniform_q120b(pa + 4 * i); - } - - // compute the function - q120_c_from_b_simple(n, (q120c*)pr, (q120b*)pa); - - for (uint64_t i = 0; i < n; ++i) { - mod_q120 ae = mod_q120::from_q120b(pa + 4 * i); - mod_q120 re = mod_q120::from_q120c(pr + 4 * i); - - ASSERT_EQ(ae, re); - } - } -} - -TEST(q120_arithmetic, q120_b_from_znx64_simple) { - for (const uint64_t n : {2, 4, 1024}) { - znx_i64 x = znx_i64::random_log2bound(n, 62); - std::vector<uint64_t> r(n * 4); - uint64_t* pr = r.data(); - - q120_b_from_znx64_simple(n, (q120b*)pr, x.data()); - - for (uint64_t i = 0; i < n; ++i) { - mod_q120 re = mod_q120::from_q120b(pr + 4 * i); - - for (uint64_t k = 0; k < 4; ++k) { - ASSERT_EQ(centermod(x.get_coeff(i), mod_q120::Qi[k]), re.a[k]); - } - } - } -} - -TEST(q120_arithmetic, q120_c_from_znx64_simple) { - for (const uint64_t n : {2, 4, 1024}) { - znx_i64 x = znx_i64::random(n); - std::vector<uint64_t> r(n * 4); - uint64_t* pr = r.data(); - - q120_c_from_znx64_simple(n, (q120c*)pr, x.data()); - - for (uint64_t i = 0; i < n; ++i) { - mod_q120 re = mod_q120::from_q120c(pr + 4 * i); - - for (uint64_t k = 0; k < 4; ++k) { - ASSERT_EQ(centermod(x.get_coeff(i), mod_q120::Qi[k]), re.a[k]); - } - } - } -} - -TEST(q120_arithmetic, q120_b_to_znx128_simple) { - for (const uint64_t n : {2, 4, 1024}) { - std::vector<uint64_t> x(n * 4); - uint64_t* px = x.data(); - - // generate some random data - for (uint64_t i = 0; i < n; ++i) { - uniform_q120b(px + 4 * i); - } - - znx_i128 r(n); - q120_b_to_znx128_simple(n, r.data(), (q120b*)px); - - for (uint64_t i = 0; i < n; ++i) { - mod_q120 xe = mod_q120::from_q120b(px + 4 * i); - for (uint64_t k = 0; k < 4; ++k) { - ASSERT_EQ(centermod((int64_t)(r.get_coeff(i) % mod_q120::Qi[k]), mod_q120::Qi[k]), xe.a[k]); - } - } - } -}
D spqlios/lib/test/spqlios_q120_ntt_bench.cpp

@@ -1,44 +0,0 @@

-#include <benchmark/benchmark.h> - -#include <cstdint> - -#include "spqlios/q120/q120_ntt.h" - -#define ARGS Arg(1 << 10)->Arg(1 << 11)->Arg(1 << 12)->Arg(1 << 13)->Arg(1 << 14)->Arg(1 << 15)->Arg(1 << 16) - -template <typeof(q120_ntt_bb_avx2) f> -void benchmark_ntt(benchmark::State& state) { - const uint64_t n = state.range(0); - q120_ntt_precomp* precomp = q120_new_ntt_bb_precomp(n); - - uint64_t* px = new uint64_t[n * 4]; - for (uint64_t i = 0; i < 4 * n; i++) { - px[i] = (rand() << 31) + rand(); - } - for (auto _ : state) { - f(precomp, (q120b*)px); - } - delete[] px; - q120_del_ntt_bb_precomp(precomp); -} - -template <typeof(q120_intt_bb_avx2) f> -void benchmark_intt(benchmark::State& state) { - const uint64_t n = state.range(0); - q120_ntt_precomp* precomp = q120_new_intt_bb_precomp(n); - - uint64_t* px = new uint64_t[n * 4]; - for (uint64_t i = 0; i < 4 * n; i++) { - px[i] = (rand() << 31) + rand(); - } - for (auto _ : state) { - f(precomp, (q120b*)px); - } - delete[] px; - q120_del_intt_bb_precomp(precomp); -} - -BENCHMARK(benchmark_ntt<q120_ntt_bb_avx2>)->Name("q120_ntt_bb_avx2")->ARGS; -BENCHMARK(benchmark_intt<q120_intt_bb_avx2>)->Name("q120_intt_bb_avx2")->ARGS; - -BENCHMARK_MAIN();
D spqlios/lib/test/spqlios_q120_ntt_test.cpp

@@ -1,174 +0,0 @@

-#include <gtest/gtest.h> - -#include <cassert> -#include <cstdint> -#include <cstdio> -#include <random> -#include <vector> - -#include "spqlios/q120/q120_common.h" -#include "spqlios/q120/q120_ntt.h" -#include "testlib/mod_q120.h" - -std::vector<mod_q120> q120_ntt(const std::vector<mod_q120>& x) { - const uint64_t n = x.size(); - - mod_q120 omega_2pow17{OMEGA1, OMEGA2, OMEGA3, OMEGA4}; - mod_q120 omega = pow(omega_2pow17, (1 << 16) / n); - - std::vector<mod_q120> res(n); - for (uint64_t i = 0; i < n; ++i) { - res[i] = x[i]; - } - - for (uint64_t i = 0; i < n; ++i) { - res[i] = res[i] * pow(omega, i); - } - - for (uint64_t nn = n; nn > 1; nn /= 2) { - const uint64_t halfnn = nn / 2; - const uint64_t m = n / halfnn; - - for (uint64_t j = 0; j < n; j += nn) { - for (uint64_t k = 0; k < halfnn; ++k) { - mod_q120 a = res[j + k]; - mod_q120 b = res[j + halfnn + k]; - - res[j + k] = a + b; - res[j + halfnn + k] = (a - b) * pow(omega, k * m); - } - } - } - - return res; -} - -std::vector<mod_q120> q120_intt(const std::vector<mod_q120>& x) { - const uint64_t n = x.size(); - - mod_q120 omega_2pow17{OMEGA1, OMEGA2, OMEGA3, OMEGA4}; - mod_q120 omega = pow(omega_2pow17, (1 << 16) / n); - - std::vector<mod_q120> res(n); - for (uint64_t i = 0; i < n; ++i) { - res[i] = x[i]; - } - - for (uint64_t nn = 2; nn <= n; nn *= 2) { - const uint64_t halfnn = nn / 2; - const uint64_t m = n / halfnn; - - for (uint64_t j = 0; j < n; j += nn) { - for (uint64_t k = 0; k < halfnn; ++k) { - mod_q120 a = res[j + k]; - mod_q120 b = res[j + halfnn + k]; - - mod_q120 bo = b * pow(omega, -k * m); - res[j + k] = a + bo; - res[j + halfnn + k] = a - bo; - } - } - } - - mod_q120 n_q120{(int64_t)n, (int64_t)n, (int64_t)n, (int64_t)n}; - mod_q120 n_inv_q120 = pow(n_q120, -1); - - for (uint64_t i = 0; i < n; ++i) { - mod_q120 po = pow(omega, -i) * n_inv_q120; - res[i] = res[i] * po; - } - - return res; -} - -class ntt : public testing::TestWithParam<uint64_t> {}; - -#ifdef __x86_64__ - -TEST_P(ntt, q120_ntt_bb_avx2) { - const uint64_t n = GetParam(); - q120_ntt_precomp* precomp = q120_new_ntt_bb_precomp(n); - - std::vector<uint64_t> x(n * 4); - uint64_t* px = x.data(); - for (uint64_t i = 0; i < 4 * n; i += 4) { - uniform_q120b(px + i); - } - - std::vector<mod_q120> x_modq(n); - for (uint64_t i = 0; i < n; ++i) { - x_modq[i] = mod_q120::from_q120b(px + 4 * i); - } - - std::vector<mod_q120> y_exp = q120_ntt(x_modq); - - q120_ntt_bb_avx2(precomp, (q120b*)px); - - for (uint64_t i = 0; i < n; ++i) { - mod_q120 comp_r = mod_q120::from_q120b(px + 4 * i); - ASSERT_EQ(comp_r, y_exp[i]) << i; - } - - q120_del_ntt_bb_precomp(precomp); -} - -TEST_P(ntt, q120_intt_bb_avx2) { - const uint64_t n = GetParam(); - q120_ntt_precomp* precomp = q120_new_intt_bb_precomp(n); - - std::vector<uint64_t> x(n * 4); - uint64_t* px = x.data(); - for (uint64_t i = 0; i < 4 * n; i += 4) { - uniform_q120b(px + i); - } - - std::vector<mod_q120> x_modq(n); - for (uint64_t i = 0; i < n; ++i) { - x_modq[i] = mod_q120::from_q120b(px + 4 * i); - } - - q120_intt_bb_avx2(precomp, (q120b*)px); - - std::vector<mod_q120> y_exp = q120_intt(x_modq); - for (uint64_t i = 0; i < n; ++i) { - mod_q120 comp_r = mod_q120::from_q120b(px + 4 * i); - ASSERT_EQ(comp_r, y_exp[i]) << i; - } - - q120_del_intt_bb_precomp(precomp); -} - -TEST_P(ntt, q120_ntt_intt_bb_avx2) { - const uint64_t n = GetParam(); - q120_ntt_precomp* precomp_ntt = q120_new_ntt_bb_precomp(n); - q120_ntt_precomp* precomp_intt = q120_new_intt_bb_precomp(n); - - std::vector<uint64_t> x(n * 4); - uint64_t* px = x.data(); - for (uint64_t i = 0; i < 4 * n; i += 4) { - uniform_q120b(px + i); - } - - std::vector<mod_q120> x_modq(n); - for (uint64_t i = 0; i < n; ++i) { - x_modq[i] = mod_q120::from_q120b(px + 4 * i); - } - - q120_ntt_bb_avx2(precomp_ntt, (q120b*)px); - q120_intt_bb_avx2(precomp_intt, (q120b*)px); - - for (uint64_t i = 0; i < n; ++i) { - mod_q120 comp_r = mod_q120::from_q120b(px + 4 * i); - ASSERT_EQ(comp_r, x_modq[i]) << i; - } - - q120_del_intt_bb_precomp(precomp_intt); - q120_del_ntt_bb_precomp(precomp_ntt); -} - -INSTANTIATE_TEST_SUITE_P(q120, ntt, - testing::Values(1, 2, 4, 16, 256, UINT64_C(1) << 10, UINT64_C(1) << 11, UINT64_C(1) << 12, - UINT64_C(1) << 13, UINT64_C(1) << 14, UINT64_C(1) << 15, UINT64_C(1) << 16), - testing::PrintToStringParamName()); - -#endif
D spqlios/lib/test/spqlios_reim4_arithmetic_bench.cpp

@@ -1,52 +0,0 @@

-#include <benchmark/benchmark.h> - -#include "spqlios/reim4/reim4_arithmetic.h" - -void init_random_values(uint64_t n, double* v) { - for (uint64_t i = 0; i < n; ++i) - v[i] = (double(rand() % (UINT64_C(1) << 14)) - (UINT64_C(1) << 13)) / (UINT64_C(1) << 12); -} - -// Run the benchmark -BENCHMARK_MAIN(); - -#undef ARGS -#define ARGS Args({47, 16384})->Args({93, 32768}) - -/* - * reim4_vec_mat1col_product - * reim4_vec_mat2col_product - * reim4_vec_mat3col_product - * reim4_vec_mat4col_product - */ - -template <uint64_t X, - void (*fnc)(const uint64_t nrows, double* const dst, const double* const u, const double* const v)> -void benchmark_reim4_vec_matXcols_product(benchmark::State& state) { - const uint64_t nrows = state.range(0); - - double* u = new double[nrows * 8]; - init_random_values(8 * nrows, u); - double* v = new double[nrows * X * 8]; - init_random_values(X * 8 * nrows, v); - double* dst = new double[X * 8]; - - for (auto _ : state) { - fnc(nrows, dst, u, v); - } - - delete[] dst; - delete[] v; - delete[] u; -} - -#undef ARGS -#define ARGS Arg(128)->Arg(1024)->Arg(4096) - -#ifdef __x86_64__ -BENCHMARK(benchmark_reim4_vec_matXcols_product<1, reim4_vec_mat1col_product_avx2>)->ARGS; -// TODO: please remove when fixed: -BENCHMARK(benchmark_reim4_vec_matXcols_product<2, reim4_vec_mat2cols_product_avx2>)->ARGS; -#endif -BENCHMARK(benchmark_reim4_vec_matXcols_product<1, reim4_vec_mat1col_product_ref>)->ARGS; -BENCHMARK(benchmark_reim4_vec_matXcols_product<2, reim4_vec_mat2cols_product_ref>)->ARGS;
D spqlios/lib/test/spqlios_reim4_arithmetic_test.cpp

@@ -1,253 +0,0 @@

-#include <gtest/gtest.h> - -#include <iostream> -#include <random> - -#include "../spqlios/reim4/reim4_arithmetic.h" -#include "test/testlib/reim4_elem.h" - -/// Actual tests - -typedef typeof(reim4_extract_1blk_from_reim_ref) reim4_extract_1blk_from_reim_f; -void test_reim4_extract_1blk_from_reim(reim4_extract_1blk_from_reim_f reim4_extract_1blk_from_reim) { - static const uint64_t numtrials = 100; - for (uint64_t m : {4, 8, 16, 1024, 4096, 32768}) { - double* v = (double*)malloc(2 * m * sizeof(double)); - double* w = (double*)malloc(8 * sizeof(double)); - reim_view vv(m, v); - for (uint64_t i = 0; i < numtrials; ++i) { - reim4_elem el = gaussian_reim4(); - uint64_t blk = rand() % (m / 4); - vv.set_blk(blk, el); - reim4_extract_1blk_from_reim(m, blk, w, v); - reim4_elem actual(w); - ASSERT_EQ(el, actual); - } - free(v); - free(w); - } -} - -TEST(reim4_arithmetic, reim4_extract_1blk_from_reim_ref) { - test_reim4_extract_1blk_from_reim(reim4_extract_1blk_from_reim_ref); -} -#ifdef __x86_64__ -TEST(reim4_arithmetic, reim4_extract_1blk_from_reim_avx) { - test_reim4_extract_1blk_from_reim(reim4_extract_1blk_from_reim_avx); -} -#endif - -typedef typeof(reim4_save_1blk_to_reim_ref) reim4_save_1blk_to_reim_f; -void test_reim4_save_1blk_to_reim(reim4_save_1blk_to_reim_f reim4_save_1blk_to_reim) { - static const uint64_t numtrials = 100; - for (uint64_t m : {4, 8, 16, 1024, 4096, 32768}) { - double* v = (double*)malloc(2 * m * sizeof(double)); - double* w = (double*)malloc(8 * sizeof(double)); - reim_view vv(m, v); - for (uint64_t i = 0; i < numtrials; ++i) { - reim4_elem el = gaussian_reim4(); - el.save_as(w); - uint64_t blk = rand() % (m / 4); - reim4_save_1blk_to_reim_ref(m, blk, v, w); - reim4_elem actual = vv.get_blk(blk); - ASSERT_EQ(el, actual); - } - free(v); - free(w); - } -} - -TEST(reim4_arithmetic, reim4_save_1blk_to_reim_ref) { test_reim4_save_1blk_to_reim(reim4_save_1blk_to_reim_ref); } -#ifdef __x86_64__ -TEST(reim4_arithmetic, reim4_save_1blk_to_reim_avx) { test_reim4_save_1blk_to_reim(reim4_save_1blk_to_reim_avx); } -#endif - -typedef typeof(reim4_extract_1blk_from_contiguous_reim_ref) reim4_extract_1blk_from_contiguous_reim_f; -void test_reim4_extract_1blk_from_contiguous_reim( - reim4_extract_1blk_from_contiguous_reim_f reim4_extract_1blk_from_contiguous_reim) { - static const uint64_t numtrials = 20; - for (uint64_t m : {4, 8, 16, 1024, 4096, 32768}) { - for (uint64_t nrows : {1, 2, 5, 128}) { - double* v = (double*)malloc(2 * m * nrows * sizeof(double)); - double* w = (double*)malloc(8 * nrows * sizeof(double)); - reim_vector_view vv(m, nrows, v); - reim4_array_view ww(nrows, w); - for (uint64_t i = 0; i < numtrials; ++i) { - uint64_t blk = rand() % (m / 4); - for (uint64_t j = 0; j < nrows; ++j) { - reim4_elem el = gaussian_reim4(); - vv.row(j).set_blk(blk, el); - } - reim4_extract_1blk_from_contiguous_reim_ref(m, nrows, blk, w, v); - for (uint64_t j = 0; j < nrows; ++j) { - reim4_elem el = vv.row(j).get_blk(blk); - reim4_elem actual = ww.get(j); - ASSERT_EQ(el, actual); - } - } - free(v); - free(w); - } - } -} - -TEST(reim4_arithmetic, reim4_extract_1blk_from_contiguous_reim_ref) { - test_reim4_extract_1blk_from_contiguous_reim(reim4_extract_1blk_from_contiguous_reim_ref); -} -#ifdef __x86_64__ -TEST(reim4_arithmetic, reim4_extract_1blk_from_contiguous_reim_avx) { - test_reim4_extract_1blk_from_contiguous_reim(reim4_extract_1blk_from_contiguous_reim_avx); -} -#endif - -// test of basic arithmetic functions - -TEST(reim4_arithmetic, add) { - reim4_elem x = gaussian_reim4(); - reim4_elem y = gaussian_reim4(); - reim4_elem expect = x + y; - reim4_elem actual; - reim4_add(actual.value, x.value, y.value); - ASSERT_EQ(actual, expect); -} - -TEST(reim4_arithmetic, mul) { - reim4_elem x = gaussian_reim4(); - reim4_elem y = gaussian_reim4(); - reim4_elem expect = x * y; - reim4_elem actual; - reim4_mul(actual.value, x.value, y.value); - ASSERT_EQ(actual, expect); -} - -TEST(reim4_arithmetic, add_mul) { - reim4_elem x = gaussian_reim4(); - reim4_elem y = gaussian_reim4(); - reim4_elem z = gaussian_reim4(); - reim4_elem expect = z; - reim4_elem actual = z; - expect += x * y; - reim4_add_mul(actual.value, x.value, y.value); - ASSERT_EQ(actual, expect) << infty_dist(expect, actual); -} - -// test of dot products - -typedef typeof(reim4_vec_mat1col_product_ref) reim4_vec_mat1col_product_f; -void test_reim4_vec_mat1col_product(reim4_vec_mat1col_product_f product) { - for (uint64_t ell : {1, 2, 5, 13, 69, 129}) { - std::vector<double> actual(8); - std::vector<double> a(ell * 8); - std::vector<double> b(ell * 8); - reim4_array_view va(ell, a.data()); - reim4_array_view vb(ell, b.data()); - reim4_array_view vactual(1, actual.data()); - // initialize random values - for (uint64_t i = 0; i < ell; ++i) { - va.set(i, gaussian_reim4()); - vb.set(i, gaussian_reim4()); - } - // compute the mat1col product - reim4_elem expect; - for (uint64_t i = 0; i < ell; ++i) { - expect += va.get(i) * vb.get(i); - } - // compute the actual product - product(ell, actual.data(), a.data(), b.data()); - // compare - ASSERT_LE(infty_dist(vactual.get(0), expect), 1e-10); - } -} - -TEST(reim4_arithmetic, reim4_vec_mat1col_product_ref) { test_reim4_vec_mat1col_product(reim4_vec_mat1col_product_ref); } -#ifdef __x86_64__ -TEST(reim4_arena, reim4_vec_mat1col_product_avx2) { test_reim4_vec_mat1col_product(reim4_vec_mat1col_product_avx2); } -#endif - -typedef typeof(reim4_vec_mat2cols_product_ref) reim4_vec_mat2col_product_f; -void test_reim4_vec_mat2cols_product(reim4_vec_mat2col_product_f product) { - for (uint64_t ell : {1, 2, 5, 13, 69, 129}) { - std::vector<double> actual(16); - std::vector<double> a(ell * 8); - std::vector<double> b(ell * 16); - reim4_array_view va(ell, a.data()); - reim4_matrix_view vb(ell, 2, b.data()); - reim4_array_view vactual(2, actual.data()); - // initialize random values - for (uint64_t i = 0; i < ell; ++i) { - va.set(i, gaussian_reim4()); - vb.set(i, 0, gaussian_reim4()); - vb.set(i, 1, gaussian_reim4()); - } - // compute the mat1col product - reim4_elem expect[2]; - for (uint64_t i = 0; i < ell; ++i) { - expect[0] += va.get(i) * vb.get(i, 0); - expect[1] += va.get(i) * vb.get(i, 1); - } - // compute the actual product - product(ell, actual.data(), a.data(), b.data()); - // compare - ASSERT_LE(infty_dist(vactual.get(0), expect[0]), 1e-10); - ASSERT_LE(infty_dist(vactual.get(1), expect[1]), 1e-10); - } -} - -TEST(reim4_arithmetic, reim4_vec_mat2cols_product_ref) { - test_reim4_vec_mat2cols_product(reim4_vec_mat2cols_product_ref); -} -#ifdef __x86_64__ -TEST(reim4_arithmetic, reim4_vec_mat2cols_product_avx2) { - test_reim4_vec_mat2cols_product(reim4_vec_mat2cols_product_avx2); -} -#endif - -// for now, we do not need avx implementations, -// so we will keep a single test function -TEST(reim4_arithmetic, reim4_vec_convolution_ref) { - for (uint64_t sizea : {1, 2, 3, 5, 8}) { - for (uint64_t sizeb : {1, 3, 6, 9, 13}) { - std::vector<double> a(8 * sizea); - std::vector<double> b(8 * sizeb); - std::vector<double> expect(8 * (sizea + sizeb - 1)); - std::vector<double> actual(8 * (sizea + sizeb - 1)); - reim4_array_view va(sizea, a.data()); - reim4_array_view vb(sizeb, b.data()); - std::vector<reim4_elem> vexpect(sizea + sizeb + 3); - reim4_array_view vactual(sizea + sizeb - 1, actual.data()); - for (uint64_t i = 0; i < sizea; ++i) { - va.set(i, gaussian_reim4()); - } - for (uint64_t j = 0; j < sizeb; ++j) { - vb.set(j, gaussian_reim4()); - } - // manual convolution - for (uint64_t i = 0; i < sizea; ++i) { - for (uint64_t j = 0; j < sizeb; ++j) { - vexpect[i + j] += va.get(i) * vb.get(j); - } - } - // partial convolution single coeff - for (uint64_t k = 0; k < sizea + sizeb + 3; ++k) { - double dest[8] = {0}; - reim4_convolution_1coeff_ref(k, dest, a.data(), sizea, b.data(), sizeb); - ASSERT_LE(infty_dist(reim4_elem(dest), vexpect[k]), 1e-10); - } - // partial convolution dual coeff - for (uint64_t k = 0; k < sizea + sizeb + 2; ++k) { - double dest[16] = {0}; - reim4_convolution_2coeff_ref(k, dest, a.data(), sizea, b.data(), sizeb); - ASSERT_LE(infty_dist(reim4_elem(dest), vexpect[k]), 1e-10); - ASSERT_LE(infty_dist(reim4_elem(dest + 8), vexpect[k + 1]), 1e-10); - } - // actual convolution - reim4_convolution_ref(actual.data(), sizea + sizeb - 1, 0, a.data(), sizea, b.data(), sizeb); - for (uint64_t k = 0; k < sizea + sizeb - 1; ++k) { - ASSERT_LE(infty_dist(vactual.get(k), vexpect[k]), 1e-10) << k; - } - } - } -} - -EXPORT void reim4_convolution_ref(double* dest, uint64_t dest_size, uint64_t dest_offset, const double* a, - uint64_t sizea, const double* b, uint64_t sizeb);
D spqlios/lib/test/spqlios_reim_conversions_test.cpp

@@ -1,115 +0,0 @@

-#include <gtest/gtest.h> -#include <spqlios/reim/reim_fft_internal.h> - -#include "testlib/test_commons.h" - -TEST(reim_conversions, reim_to_tnx) { - for (uint32_t m : {1, 2, 64, 128, 512}) { - for (double divisor : {1, 2, int(m)}) { - for (uint32_t log2overhead : {1, 2, 10, 18, 35, 42}) { - double maxdiff = pow(2., log2overhead - 50); - std::vector<double> data(2 * m); - std::vector<double> dout(2 * m); - for (uint64_t i = 0; i < 2 * m; ++i) { - data[i] = (uniform_f64_01() - 0.5) * pow(2., log2overhead + 1) * divisor; - } - REIM_TO_TNX_PRECOMP* p = new_reim_to_tnx_precomp(m, divisor, 18); - reim_to_tnx_ref(p, dout.data(), data.data()); - for (uint64_t i = 0; i < 2 * m; ++i) { - ASSERT_LE(fabs(dout[i]), 0.5); - double diff = dout[i] - data[i] / divisor; - double fracdiff = diff - rint(diff); - ASSERT_LE(fabs(fracdiff), maxdiff); - } - delete_reim_to_tnx_precomp(p); - } - } - } -} - -#ifdef __x86_64__ -TEST(reim_conversions, reim_to_tnx_ref_vs_avx) { - for (uint32_t m : {8, 16, 64, 128, 512}) { - for (double divisor : {1, 2, int(m)}) { - for (uint32_t log2overhead : {1, 2, 10, 18, 35, 42}) { - // double maxdiff = pow(2., log2overhead - 50); - std::vector<double> data(2 * m); - std::vector<double> dout1(2 * m); - std::vector<double> dout2(2 * m); - for (uint64_t i = 0; i < 2 * m; ++i) { - data[i] = (uniform_f64_01() - 0.5) * pow(2., log2overhead + 1) * divisor; - } - REIM_TO_TNX_PRECOMP* p = new_reim_to_tnx_precomp(m, divisor, 18); - reim_to_tnx_ref(p, dout1.data(), data.data()); - reim_to_tnx_avx(p, dout2.data(), data.data()); - for (uint64_t i = 0; i < 2 * m; ++i) { - ASSERT_LE(fabs(dout1[i] - dout2[i]), 0.); - } - delete_reim_to_tnx_precomp(p); - } - } - } -} -#endif - -typedef typeof(reim_from_znx64_ref) reim_from_znx64_f; - -void test_reim_from_znx64(reim_from_znx64_f reim_from_znx64, uint64_t maxbnd) { - for (uint32_t m : {4, 8, 16, 64, 16384}) { - REIM_FROM_ZNX64_PRECOMP* p = new_reim_from_znx64_precomp(m, maxbnd); - std::vector<int64_t> data(2 * m); - std::vector<double> dout(2 * m); - for (uint64_t i = 0; i < 2 * m; ++i) { - int64_t magnitude = int64_t(uniform_u64() % (maxbnd + 1)); - data[i] = uniform_i64() >> (63 - magnitude); - REQUIRE_DRAMATICALLY(abs(data[i]) <= (INT64_C(1) << magnitude), "pb"); - } - reim_from_znx64(p, dout.data(), data.data()); - for (uint64_t i = 0; i < 2 * m; ++i) { - ASSERT_EQ(dout[i], double(data[i])) << dout[i] << " " << data[i]; - } - delete_reim_from_znx64_precomp(p); - } -} - -TEST(reim_conversions, reim_from_znx64) { - for (uint64_t maxbnd : {50}) { - test_reim_from_znx64(reim_from_znx64, maxbnd); - } -} -TEST(reim_conversions, reim_from_znx64_ref) { test_reim_from_znx64(reim_from_znx64_ref, 50); } -#ifdef __x86_64__ -TEST(reim_conversions, reim_from_znx64_avx2_bnd50_fma) { test_reim_from_znx64(reim_from_znx64_bnd50_fma, 50); } -#endif - -typedef typeof(reim_to_znx64_ref) reim_to_znx64_f; - -void test_reim_to_znx64(reim_to_znx64_f reim_to_znx64_fcn, int64_t maxbnd) { - for (uint32_t m : {4, 8, 16, 64, 16384}) { - for (double divisor : {1, 2, int(m)}) { - REIM_TO_ZNX64_PRECOMP* p = new_reim_to_znx64_precomp(m, divisor, maxbnd); - std::vector<double> data(2 * m); - std::vector<int64_t> dout(2 * m); - for (uint64_t i = 0; i < 2 * m; ++i) { - int64_t magnitude = int64_t(uniform_u64() % (maxbnd + 11)) - 10; - data[i] = (uniform_f64_01() - 0.5) * pow(2., magnitude + 1) * divisor; - } - reim_to_znx64_fcn(p, dout.data(), data.data()); - for (uint64_t i = 0; i < 2 * m; ++i) { - ASSERT_LE(dout[i] - data[i] / divisor, 0.5) << dout[i] << " " << data[i]; - } - delete_reim_to_znx64_precomp(p); - } - } -} - -TEST(reim_conversions, reim_to_znx64) { - for (uint64_t maxbnd : {63, 50}) { - test_reim_to_znx64(reim_to_znx64, maxbnd); - } -} -TEST(reim_conversions, reim_to_znx64_ref) { test_reim_to_znx64(reim_to_znx64_ref, 63); } -#ifdef __x86_64__ -TEST(reim_conversions, reim_to_znx64_avx2_bnd63_fma) { test_reim_to_znx64(reim_to_znx64_avx2_bnd63_fma, 63); } -TEST(reim_conversions, reim_to_znx64_avx2_bnd50_fma) { test_reim_to_znx64(reim_to_znx64_avx2_bnd50_fma, 50); } -#endif
D spqlios/lib/test/spqlios_reim_test.cpp

@@ -1,477 +0,0 @@

-#include <inttypes.h> - -#include <cmath> - -#include "gtest/gtest.h" -#include "spqlios/commons_private.h" -#include "spqlios/cplx/cplx_fft_internal.h" -#include "spqlios/reim/reim_fft_internal.h" -#include "spqlios/reim/reim_fft_private.h" - -#ifdef __x86_64__ -TEST(fft, reim_fft_avx2_vs_fft_reim_ref) { - for (uint64_t nn : {16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - // CPLX_FFT_PRECOMP* tables = new_cplx_fft_precomp(m, 0); - REIM_FFT_PRECOMP* reimtables = new_reim_fft_precomp(m, 0); - CPLX* a = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn / 2; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - } - memcpy(a1, a, nn / 2 * sizeof(CPLX)); - memcpy(a2, a, nn / 2 * sizeof(CPLX)); - reim_fft_ref(reimtables, a2); - reim_fft_avx2_fma(reimtables, a1); - double d = 0; - for (uint32_t i = 0; i < nn / 2; i++) { - double dre = fabs(a1[i] - a2[i]); - double dim = fabs(a1[nn / 2 + i] - a2[nn / 2 + i]); - if (dre > d) d = dre; - if (dim > d) d = dim; - ASSERT_LE(d, nn * 1e-10) << nn; - } - ASSERT_LE(d, nn * 1e-10) << nn; - spqlios_free(a); - spqlios_free(a1); - spqlios_free(a2); - // delete_cplx_fft_precomp(tables); - delete_reim_fft_precomp(reimtables); - } -} -#endif - -#ifdef __x86_64__ -TEST(fft, reim_ifft_avx2_vs_reim_ifft_ref) { - for (uint64_t nn : {16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - // CPLX_FFT_PRECOMP* tables = new_cplx_fft_precomp(m, 0); - REIM_IFFT_PRECOMP* reimtables = new_reim_ifft_precomp(m, 0); - CPLX* a = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn / 2; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - } - memcpy(a1, a, nn / 2 * sizeof(CPLX)); - memcpy(a2, a, nn / 2 * sizeof(CPLX)); - reim_ifft_ref(reimtables, a2); - reim_ifft_avx2_fma(reimtables, a1); - double d = 0; - for (uint32_t i = 0; i < nn / 2; i++) { - double dre = fabs(a1[i] - a2[i]); - double dim = fabs(a1[nn / 2 + i] - a2[nn / 2 + i]); - if (dre > d) d = dre; - if (dim > d) d = dim; - ASSERT_LE(d, 1e-8); - } - ASSERT_LE(d, 1e-8); - spqlios_free(a); - spqlios_free(a1); - spqlios_free(a2); - // delete_cplx_fft_precomp(tables); - delete_reim_fft_precomp(reimtables); - } -} -#endif - -#ifdef __x86_64__ -TEST(fft, reim_vecfft_addmul_fma_vs_ref) { - for (uint64_t nn : {16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - REIM_FFTVEC_ADDMUL_PRECOMP* tbl = new_reim_fftvec_addmul_precomp(m); - ASSERT_TRUE(tbl != nullptr); - double* a1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* b1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* b2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn; i++) { - a1[i] = (rand() % p) - p / 2; // between -p/2 and p/2 - b1[i] = (rand() % p) - p / 2; - r1[i] = (rand() % p) - p / 2; - } - memcpy(a2, a1, nn / 2 * sizeof(CPLX)); - memcpy(b2, b1, nn / 2 * sizeof(CPLX)); - memcpy(r2, r1, nn / 2 * sizeof(CPLX)); - reim_fftvec_addmul_ref(tbl, r1, a1, b1); - reim_fftvec_addmul_fma(tbl, r2, a2, b2); - double d = 0; - for (uint32_t i = 0; i < nn; i++) { - double di = fabs(r1[i] - r2[i]); - if (di > d) d = di; - ASSERT_LE(d, 1e-8); - } - ASSERT_LE(d, 1e-8); - spqlios_free(a1); - spqlios_free(a2); - spqlios_free(b1); - spqlios_free(b2); - spqlios_free(r1); - spqlios_free(r2); - delete_reim_fftvec_addmul_precomp(tbl); - } -} -#endif - -#ifdef __x86_64__ -TEST(fft, reim_vecfft_mul_fma_vs_ref) { - for (uint64_t nn : {16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - REIM_FFTVEC_MUL_PRECOMP* tbl = new_reim_fftvec_mul_precomp(m); - double* a1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* b1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* b2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn; i++) { - a1[i] = (rand() % p) - p / 2; // between -p/2 and p/2 - b1[i] = (rand() % p) - p / 2; - r1[i] = (rand() % p) - p / 2; - } - memcpy(a2, a1, nn / 2 * sizeof(CPLX)); - memcpy(b2, b1, nn / 2 * sizeof(CPLX)); - memcpy(r2, r1, nn / 2 * sizeof(CPLX)); - reim_fftvec_mul_ref(tbl, r1, a1, b1); - reim_fftvec_mul_fma(tbl, r2, a2, b2); - double d = 0; - for (uint32_t i = 0; i < nn; i++) { - double di = fabs(r1[i] - r2[i]); - if (di > d) d = di; - ASSERT_LE(d, 1e-8); - } - ASSERT_LE(d, 1e-8); - spqlios_free(a1); - spqlios_free(a2); - spqlios_free(b1); - spqlios_free(b2); - spqlios_free(r1); - spqlios_free(r2); - delete_reim_fftvec_mul_precomp(tbl); - } -} -#endif - -typedef void (*FILL_REIM_FFT_OMG_F)(const double entry_pwr, double** omg); -typedef void (*REIM_FFT_F)(double* dre, double* dim, const void* omega); - -// template to test a fixed-dimension fft vs. naive -template <uint64_t N> -void test_reim_fft_ref_vs_naive(FILL_REIM_FFT_OMG_F fill_omega_f, REIM_FFT_F reim_fft_f) { - double om[N]; - double data[2 * N]; - double datacopy[2 * N]; - double* omg = om; - fill_omega_f(0.25, &omg); - ASSERT_EQ(omg - om, ptrdiff_t(N)); // it may depend on N - for (uint64_t i = 0; i < N; ++i) { - datacopy[i] = data[i] = (rand() % 100) - 50; - datacopy[N + i] = data[N + i] = (rand() % 100) - 50; - } - reim_fft_f(datacopy, datacopy + N, om); - reim_naive_fft(N, 0.25, data, data + N); - double d = 0; - for (uint64_t i = 0; i < 2 * N; ++i) { - d += fabs(datacopy[i] - data[i]); - } - ASSERT_LE(d, 1e-7); -} - -template <uint64_t N> -void test_reim_fft_ref_vs_accel(REIM_FFT_F reim_fft_ref_f, REIM_FFT_F reim_fft_accel_f) { - double om[N]; - double data[2 * N]; - double datacopy[2 * N]; - for (uint64_t i = 0; i < N; ++i) { - om[i] = (rand() % 100) - 50; - datacopy[i] = data[i] = (rand() % 100) - 50; - datacopy[N + i] = data[N + i] = (rand() % 100) - 50; - } - reim_fft_ref_f(datacopy, datacopy + N, om); - reim_fft_accel_f(data, data + N, om); - double d = 0; - for (uint64_t i = 0; i < 2 * N; ++i) { - d += fabs(datacopy[i] - data[i]); - } - if (d > 1e-15) { - for (uint64_t i = 0; i < N; ++i) { - printf("%" PRId64 " %lf %lf %lf %lf\n", i, data[i], data[N + i], datacopy[i], datacopy[N + i]); - } - ASSERT_LE(d, 0); - } -} - -TEST(fft, reim_fft16_ref_vs_naive) { test_reim_fft_ref_vs_naive<16>(fill_reim_fft16_omegas, reim_fft16_ref); } -#ifdef __aarch64__ -TEST(fft, reim_fft16_neon_vs_naive) { test_reim_fft_ref_vs_naive<16>(fill_reim_fft16_omegas_neon, reim_fft16_neon); } -#endif - -#ifdef __x86_64__ -TEST(fft, reim_fft16_ref_vs_fma) { test_reim_fft_ref_vs_accel<16>(reim_fft16_ref, reim_fft16_avx_fma); } -#endif - -#ifdef __aarch64__ -static void reim_fft16_ref_neon_pom(double* dre, double* dim, const void* omega) { - const double* pom = (double*) omega; - // put the omegas in neon order - double x_pom[] = { - pom[0], pom[1], pom[2], pom[3], - pom[4],pom[5], pom[6], pom[7], - pom[8], pom[10],pom[12], pom[14], - pom[9], pom[11],pom[13], pom[15] - }; - reim_fft16_ref(dre, dim, x_pom); -} -TEST(fft, reim_fft16_ref_vs_neon) { test_reim_fft_ref_vs_accel<16>(reim_fft16_ref_neon_pom, reim_fft16_neon); } -#endif - -TEST(fft, reim_fft8_ref_vs_naive) { test_reim_fft_ref_vs_naive<8>(fill_reim_fft8_omegas, reim_fft8_ref); } - -#ifdef __x86_64__ -TEST(fft, reim_fft8_ref_vs_fma) { test_reim_fft_ref_vs_accel<8>(reim_fft8_ref, reim_fft8_avx_fma); } -#endif - -TEST(fft, reim_fft4_ref_vs_naive) { test_reim_fft_ref_vs_naive<4>(fill_reim_fft4_omegas, reim_fft4_ref); } - -#ifdef __x86_64__ -TEST(fft, reim_fft4_ref_vs_fma) { test_reim_fft_ref_vs_accel<4>(reim_fft4_ref, reim_fft4_avx_fma); } -#endif - -TEST(fft, reim_fft2_ref_vs_naive) { test_reim_fft_ref_vs_naive<2>(fill_reim_fft2_omegas, reim_fft2_ref); } - -TEST(fft, reim_fft_bfs_16_ref_vs_naive) { - for (const uint64_t m : {16, 32, 64, 128, 256, 512, 1024, 2048}) { - std::vector<double> om(2 * m); - std::vector<double> data(2 * m); - std::vector<double> datacopy(2 * m); - double* omg = om.data(); - fill_reim_fft_bfs_16_omegas(m, 0.25, &omg); - ASSERT_LE(omg - om.data(), ptrdiff_t(2 * m)); // it may depend on m - for (uint64_t i = 0; i < m; ++i) { - datacopy[i] = data[i] = (rand() % 100) - 50; - datacopy[m + i] = data[m + i] = (rand() % 100) - 50; - } - omg = om.data(); - reim_fft_bfs_16_ref(m, datacopy.data(), datacopy.data() + m, &omg); - reim_naive_fft(m, 0.25, data.data(), data.data() + m); - double d = 0; - for (uint64_t i = 0; i < 2 * m; ++i) { - d += fabs(datacopy[i] - data[i]); - } - ASSERT_LE(d, 1e-7); - } -} - -TEST(fft, reim_fft_rec_16_ref_vs_naive) { - for (const uint64_t m : {2048, 4096, 8192, 32768, 65536}) { - std::vector<double> om(2 * m); - std::vector<double> data(2 * m); - std::vector<double> datacopy(2 * m); - double* omg = om.data(); - fill_reim_fft_rec_16_omegas(m, 0.25, &omg); - ASSERT_LE(omg - om.data(), ptrdiff_t(2 * m)); // it may depend on m - for (uint64_t i = 0; i < m; ++i) { - datacopy[i] = data[i] = (rand() % 100) - 50; - datacopy[m + i] = data[m + i] = (rand() % 100) - 50; - } - omg = om.data(); - reim_fft_rec_16_ref(m, datacopy.data(), datacopy.data() + m, &omg); - reim_naive_fft(m, 0.25, data.data(), data.data() + m); - double d = 0; - for (uint64_t i = 0; i < 2 * m; ++i) { - d += fabs(datacopy[i] - data[i]); - } - ASSERT_LE(d, 1e-5); - } -} - -TEST(fft, reim_fft_ref_vs_naive) { - for (const uint64_t m : {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 32768, 65536}) { - std::vector<double> om(2 * m); - std::vector<double> data(2 * m); - std::vector<double> datacopy(2 * m); - REIM_FFT_PRECOMP* precomp = new_reim_fft_precomp(m, 0); - for (uint64_t i = 0; i < m; ++i) { - datacopy[i] = data[i] = (rand() % 100) - 50; - datacopy[m + i] = data[m + i] = (rand() % 100) - 50; - } - reim_fft_ref(precomp, datacopy.data()); - reim_naive_fft(m, 0.25, data.data(), data.data() + m); - double d = 0; - for (uint64_t i = 0; i < 2 * m; ++i) { - d += fabs(datacopy[i] - data[i]); - } - ASSERT_LE(d, 1e-5) << m; - delete_reim_fft_precomp(precomp); - } -} - -#ifdef __aarch64__ -EXPORT REIM_FFT_PRECOMP* new_reim_fft_precomp_neon(uint32_t m, uint32_t num_buffers); -EXPORT void reim_fft_neon(const REIM_FFT_PRECOMP* precomp, double* d); -TEST(fft, reim_fft_neon_vs_naive) { - for (const uint64_t m : {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 32768, 65536}) { - std::vector<double> om(2 * m); - std::vector<double> data(2 * m); - std::vector<double> datacopy(2 * m); - REIM_FFT_PRECOMP* precomp = new_reim_fft_precomp_neon(m, 0); - for (uint64_t i = 0; i < m; ++i) { - datacopy[i] = data[i] = (rand() % 100) - 50; - datacopy[m + i] = data[m + i] = (rand() % 100) - 50; - } - reim_fft_neon(precomp, datacopy.data()); - reim_naive_fft(m, 0.25, data.data(), data.data() + m); - double d = 0; - for (uint64_t i = 0; i < 2 * m; ++i) { - d += fabs(datacopy[i] - data[i]); - } - ASSERT_LE(d, 1e-5) << m; - delete_reim_fft_precomp(precomp); - } -} -#endif - -typedef void (*FILL_REIM_IFFT_OMG_F)(const double entry_pwr, double** omg); -typedef void (*REIM_IFFT_F)(double* dre, double* dim, const void* omega); - -// template to test a fixed-dimension fft vs. naive -template <uint64_t N> -void test_reim_ifft_ref_vs_naive(FILL_REIM_IFFT_OMG_F fill_omega_f, REIM_IFFT_F reim_ifft_f) { - double om[N]; - double data[2 * N]; - double datacopy[2 * N]; - double* omg = om; - fill_omega_f(0.25, &omg); - ASSERT_EQ(omg - om, ptrdiff_t(N)); // it may depend on N - for (uint64_t i = 0; i < N; ++i) { - datacopy[i] = data[i] = (rand() % 100) - 50; - datacopy[N + i] = data[N + i] = (rand() % 100) - 50; - } - reim_ifft_f(datacopy, datacopy + N, om); - reim_naive_ifft(N, 0.25, data, data + N); - double d = 0; - for (uint64_t i = 0; i < 2 * N; ++i) { - d += fabs(datacopy[i] - data[i]); - } - ASSERT_LE(d, 1e-7); -} - -template <uint64_t N> -void test_reim_ifft_ref_vs_accel(REIM_IFFT_F reim_ifft_ref_f, REIM_IFFT_F reim_ifft_accel_f) { - double om[N]; - double data[2 * N]; - double datacopy[2 * N]; - for (uint64_t i = 0; i < N; ++i) { - om[i] = (rand() % 100) - 50; - datacopy[i] = data[i] = (rand() % 100) - 50; - datacopy[N + i] = data[N + i] = (rand() % 100) - 50; - } - reim_ifft_ref_f(datacopy, datacopy + N, om); - reim_ifft_accel_f(data, data + N, om); - double d = 0; - for (uint64_t i = 0; i < 2 * N; ++i) { - d += fabs(datacopy[i] - data[i]); - } - if (d > 1e-15) { - for (uint64_t i = 0; i < N; ++i) { - printf("%" PRId64 " %lf %lf %lf %lf\n", i, data[i], data[N + i], datacopy[i], datacopy[N + i]); - } - ASSERT_LE(d, 0); - } -} - -TEST(fft, reim_ifft16_ref_vs_naive) { test_reim_ifft_ref_vs_naive<16>(fill_reim_ifft16_omegas, reim_ifft16_ref); } - -#ifdef __x86_64__ -TEST(fft, reim_ifft16_ref_vs_fma) { test_reim_ifft_ref_vs_accel<16>(reim_ifft16_ref, reim_ifft16_avx_fma); } -#endif - -TEST(fft, reim_ifft8_ref_vs_naive) { test_reim_ifft_ref_vs_naive<8>(fill_reim_ifft8_omegas, reim_ifft8_ref); } - -#ifdef __x86_64__ -TEST(fft, reim_ifft8_ref_vs_fma) { test_reim_ifft_ref_vs_accel<8>(reim_ifft8_ref, reim_ifft8_avx_fma); } -#endif - -TEST(fft, reim_ifft4_ref_vs_naive) { test_reim_ifft_ref_vs_naive<4>(fill_reim_ifft4_omegas, reim_ifft4_ref); } - -#ifdef __x86_64__ -TEST(fft, reim_ifft4_ref_vs_fma) { test_reim_ifft_ref_vs_accel<4>(reim_ifft4_ref, reim_ifft4_avx_fma); } -#endif - -TEST(fft, reim_ifft2_ref_vs_naive) { test_reim_ifft_ref_vs_naive<2>(fill_reim_ifft2_omegas, reim_ifft2_ref); } - -TEST(fft, reim_ifft_bfs_16_ref_vs_naive) { - for (const uint64_t m : {16, 32, 64, 128, 256, 512, 1024, 2048}) { - std::vector<double> om(2 * m); - std::vector<double> data(2 * m); - std::vector<double> datacopy(2 * m); - double* omg = om.data(); - fill_reim_ifft_bfs_16_omegas(m, 0.25, &omg); - ASSERT_LE(omg - om.data(), ptrdiff_t(2 * m)); // it may depend on m - for (uint64_t i = 0; i < m; ++i) { - datacopy[i] = data[i] = (rand() % 100) - 50; - datacopy[m + i] = data[m + i] = (rand() % 100) - 50; - } - omg = om.data(); - reim_ifft_bfs_16_ref(m, datacopy.data(), datacopy.data() + m, &omg); - reim_naive_ifft(m, 0.25, data.data(), data.data() + m); - double d = 0; - for (uint64_t i = 0; i < 2 * m; ++i) { - d += fabs(datacopy[i] - data[i]); - } - ASSERT_LE(d, 1e-7); - } -} - -TEST(fft, reim_ifft_rec_16_ref_vs_naive) { - for (const uint64_t m : {2048, 4096, 8192, 32768, 65536}) { - std::vector<double> om(2 * m); - std::vector<double> data(2 * m); - std::vector<double> datacopy(2 * m); - double* omg = om.data(); - fill_reim_ifft_rec_16_omegas(m, 0.25, &omg); - ASSERT_LE(omg - om.data(), ptrdiff_t(2 * m)); // it may depend on m - for (uint64_t i = 0; i < m; ++i) { - datacopy[i] = data[i] = (rand() % 100) - 50; - datacopy[m + i] = data[m + i] = (rand() % 100) - 50; - } - omg = om.data(); - reim_ifft_rec_16_ref(m, datacopy.data(), datacopy.data() + m, &omg); - reim_naive_ifft(m, 0.25, data.data(), data.data() + m); - double d = 0; - for (uint64_t i = 0; i < 2 * m; ++i) { - d += fabs(datacopy[i] - data[i]); - } - ASSERT_LE(d, 1e-5); - } -} - -TEST(fft, reim_ifft_ref_vs_naive) { - for (const uint64_t m : {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 32768, 65536}) { - std::vector<double> om(2 * m); - std::vector<double> data(2 * m); - std::vector<double> datacopy(2 * m); - REIM_IFFT_PRECOMP* precomp = new_reim_ifft_precomp(m, 0); - for (uint64_t i = 0; i < m; ++i) { - datacopy[i] = data[i] = (rand() % 100) - 50; - datacopy[m + i] = data[m + i] = (rand() % 100) - 50; - } - reim_ifft_ref(precomp, datacopy.data()); - reim_naive_ifft(m, 0.25, data.data(), data.data() + m); - double d = 0; - for (uint64_t i = 0; i < 2 * m; ++i) { - d += fabs(datacopy[i] - data[i]); - } - ASSERT_LE(d, 1e-5) << m; - delete_reim_ifft_precomp(precomp); - } -}
D spqlios/lib/test/spqlios_svp_product_test.cpp

@@ -1,28 +0,0 @@

-#include <gtest/gtest.h> - -#include "../spqlios/arithmetic/vec_znx_arithmetic_private.h" -#include "testlib/fft64_dft.h" -#include "testlib/fft64_layouts.h" -#include "testlib/polynomial_vector.h" - -// todo: remove when registered -typedef typeof(fft64_svp_prepare_ref) SVP_PREPARE_F; - -void test_fft64_svp_prepare(SVP_PREPARE_F svp_prepare) { - for (uint64_t n : {2, 4, 8, 64, 128}) { - MODULE* module = new_module_info(n, FFT64); - znx_i64 in = znx_i64::random_log2bound(n, 40); - fft64_svp_ppol_layout out(n); - reim_fft64vec expect = simple_fft64(in); - svp_prepare(module, out.data, in.data()); - const double* ed = (double*)expect.data(); - const double* ac = (double*)out.data; - for (uint64_t i = 0; i < n; ++i) { - ASSERT_LE(abs(ed[i] - ac[i]), 1e-10) << i << n; - } - delete_module_info(module); - } -} - -TEST(svp_prepare, fft64_svp_prepare_ref) { test_fft64_svp_prepare(fft64_svp_prepare_ref); } -TEST(svp_prepare, svp_prepare) { test_fft64_svp_prepare(svp_prepare); }
D spqlios/lib/test/spqlios_svp_test.cpp

@@ -1,47 +0,0 @@

-#include <gtest/gtest.h> - -#include "../spqlios/arithmetic/vec_znx_arithmetic_private.h" -#include "testlib/fft64_dft.h" -#include "testlib/fft64_layouts.h" -#include "testlib/polynomial_vector.h" - -void test_fft64_svp_apply_dft(SVP_APPLY_DFT_F svp) { - for (uint64_t n : {2, 4, 8, 64, 128}) { - MODULE* module = new_module_info(n, FFT64); - // poly 1 to multiply - create and prepare - fft64_svp_ppol_layout ppol(n); - ppol.fill_random(1.); - for (uint64_t sa : {3, 5, 8}) { - for (uint64_t sr : {3, 5, 8}) { - uint64_t a_sl = n + uniform_u64_bits(2); - // poly 2 to multiply - znx_vec_i64_layout a(n, sa, a_sl); - a.fill_random(19); - // original operation result - fft64_vec_znx_dft_layout res(n, sr); - thash hash_a_before = a.content_hash(); - thash hash_ppol_before = ppol.content_hash(); - svp(module, res.data, sr, ppol.data, a.data(), sa, a_sl); - ASSERT_EQ(a.content_hash(), hash_a_before); - ASSERT_EQ(ppol.content_hash(), hash_ppol_before); - // create expected value - reim_fft64vec ppo = ppol.get_copy(); - std::vector<reim_fft64vec> expect(sr); - for (uint64_t i = 0; i < sr; ++i) { - expect[i] = ppo * simple_fft64(a.get_copy_zext(i)); - } - // this is the largest precision we can safely expect - double prec_expect = n * pow(2., 19 - 52); - for (uint64_t i = 0; i < sr; ++i) { - reim_fft64vec actual = res.get_copy_zext(i); - ASSERT_LE(infty_dist(actual, expect[i]), prec_expect); - } - } - } - - delete_module_info(module); - } -} - -TEST(fft64_svp_apply_dft, svp_apply_dft) { test_fft64_svp_apply_dft(svp_apply_dft); } -TEST(fft64_svp_apply_dft, fft64_svp_apply_dft_ref) { test_fft64_svp_apply_dft(fft64_svp_apply_dft_ref); }
D spqlios/lib/test/spqlios_test.cpp

@@ -1,493 +0,0 @@

-#include <cassert> -#include <cinttypes> -#include <cmath> -#include <complex> -#include <cstdint> -#include <cstdlib> -#include <iostream> -#include <string> -#include <vector> - -#include "gtest/gtest.h" -#include "spqlios/cplx/cplx_fft_internal.h" - -using namespace std; - -/*namespace { -bool very_close(const double& a, const double& b) { - bool reps = (abs(a - b) < 1e-5); - if (!reps) { - cerr << "not close: " << a << " vs. " << b << endl; - } - return reps; -} - -}*/ // namespace - -TEST(fft, fftvec_convolution) { - uint64_t nn = 65536; // vary accross (8192, 16384), 32768, 65536 - static const uint64_t k = 18; // vary from 10 to 20 - // double* buf_fft = fft_precomp_get_buffer(tables, 0); - // double* buf_ifft = ifft_precomp_get_buffer(itables, 0); - double* a = (double*)spqlios_alloc_custom_align(32, nn * 8); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn * 8); - double* b = (double*)spqlios_alloc_custom_align(32, nn * 8); - double* dist_vector = (double*)spqlios_alloc_custom_align(32, nn * 8); - int64_t p = UINT64_C(1) << k; - printf("p size: %" PRId64 "\n", p); - for (uint32_t i = 0; i < nn; i++) { - a[i] = (rand() % p) - p / 2; // between -p/2 and p/2 - b[i] = (rand() % p) - p / 2; - a2[i] = 0; - } - cplx_fft_simple(nn / 2, a); - cplx_fft_simple(nn / 2, b); - cplx_fftvec_addmul_simple(nn / 2, a2, a, b); - cplx_ifft_simple(nn / 2, a2); // normalization is missing - double distance = 0; - // for (int32_t i = 0; i < 10; i++) { - // printf("%lf %lf\n", a2[i], a2[i] / (nn / 2.)); - //} - for (uint32_t i = 0; i < nn; i++) { - double curdist = fabs(a2[i] / (nn / 2.) - rint(a2[i] / (nn / 2.))); - if (distance < curdist) distance = curdist; - dist_vector[i] = a2[i] / (nn / 2.) - rint(a2[i] / (nn / 2.)); - } - printf("distance: %lf\n", distance); - ASSERT_LE(distance, 0.5); // switch from previous 0.1 to 0.5 per experiment 1 reqs - // double a3[] = {2,4,4,4,5,5,7,9}; //instead of dist_vector, for test - // nn = 8; - double mean = 0; - for (uint32_t i = 0; i < nn; i++) { - mean = mean + dist_vector[i]; - } - mean = mean / nn; - double variance = 0; - for (uint32_t i = 0; i < nn; i++) { - variance = variance + pow((mean - dist_vector[i]), 2); - } - double stdev = sqrt(variance / nn); - printf("stdev: %lf\n", stdev); - - spqlios_free(a); - spqlios_free(b); - spqlios_free(a2); - spqlios_free(dist_vector); -} - -typedef double CPLX[2]; -EXPORT uint32_t revbits(uint32_t i, uint32_t v); - -void cplx_zero(CPLX r); -void cplx_addmul(CPLX r, const CPLX a, const CPLX b); - -void halfcfft_eval(CPLX res, uint32_t nn, uint32_t k, const CPLX* coeffs, const CPLX* powomegas); -void halfcfft_naive(uint32_t nn, CPLX* data); - -EXPORT void cplx_set(CPLX, const CPLX); -EXPORT void citwiddle(CPLX, CPLX, const CPLX); -EXPORT void invcitwiddle(CPLX, CPLX, const CPLX); -EXPORT void ctwiddle(CPLX, CPLX, const CPLX); -EXPORT void invctwiddle(CPLX, CPLX, const CPLX); - -#include "../spqlios/cplx/cplx_fft_private.h" -#include "../spqlios/reim/reim_fft_internal.h" -#include "../spqlios/reim/reim_fft_private.h" -#include "../spqlios/reim4/reim4_fftvec_internal.h" -#include "../spqlios/reim4/reim4_fftvec_private.h" - -TEST(fft, simple_fft_test) { // test for checking the simple_fft api - uint64_t nn = 8; // vary accross (8192, 16384), 32768, 65536 - // double* buf_fft = fft_precomp_get_buffer(tables, 0); - // double* buf_ifft = ifft_precomp_get_buffer(itables, 0); - - // define the complex coefficients of two polynomials mod X^4-i - double a[4][2] = {{1.1, 2.2}, {3.3, 4.4}, {5.5, 6.6}, {7.7, 8.8}}; - double b[4][2] = {{9., 10.}, {11., 12.}, {13., 14.}, {15., 16.}}; - double c[4][2]; // for the result - double a2[4][2]; // for testing inverse fft - memcpy(a2, a, 8 * nn); - cplx_fft_simple(4, a); - cplx_fft_simple(4, b); - cplx_fftvec_mul_simple(4, c, a, b); - cplx_ifft_simple(4, c); - // c contains the complex coefficients 4.a*b mod X^4-i - cplx_ifft_simple(4, a); - - double distance = 0; - for (uint32_t i = 0; i < nn / 2; i++) { - double dist = fabs(a[i][0] / 4. - a2[i][0]); - if (distance < dist) distance = dist; - dist = fabs(a[i][1] / 4. - a2[i][1]); - if (distance < dist) distance = dist; - } - printf("distance: %lf\n", distance); - ASSERT_LE(distance, 0.1); // switch from previous 0.1 to 0.5 per experiment 1 reqs - - for (uint32_t i = 0; i < nn / 4; i++) { - printf("%lf %lf\n", a2[i][0], a[i][0] / (nn / 2.)); - printf("%lf %lf\n", a2[i][1], a[i][1] / (nn / 2.)); - } -} - -TEST(fft, reim_test) { - // double a[16] __attribute__ ((aligned(32)))= {1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9.9,10.,11.,12.,13.,14.,15.,16.}; - // double b[16] __attribute__ ((aligned(32)))= {17.,18.,19.,20.,21.,22.,23.,24.,25.,26.,27.,28.,29.,30., 31.,32.}; - // double c[16] __attribute__ ((aligned(32))); // for the result in reference layout - double a[16] = {1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9, 10., 11., 12., 13., 14., 15., 16.}; - double b[16] = {17., 18., 19., 20., 21., 22., 23., 24., 25., 26., 27., 28., 29., 30., 31., 32.}; - double c[16]; // for the result in reference layout - reim_fft_simple(8, a); - reim_fft_simple(8, b); - reim_fftvec_mul_simple(8, c, a, b); - reim_ifft_simple(8, c); -} - -TEST(fft, reim_vs_regular_layout_mul_test) { - uint64_t nn = 16; - - // define the complex coefficients of two polynomials mod X^8-i - - double a1[8][2] __attribute__((aligned(32))) = {{1.1, 2.2}, {3.3, 4.4}, {5.5, 6.6}, {7.7, 8.8}, - {9.9, 10.}, {11., 12.}, {13., 14.}, {15., 16.}}; - double b1[8][2] __attribute__((aligned(32))) = {{17., 18.}, {19., 20.}, {21., 22.}, {23., 24.}, - {25., 26.}, {27., 28.}, {29., 30.}, {31., 32.}}; - double c1[8][2] __attribute__((aligned(32))); // for the result - double c2[16] __attribute__((aligned(32))); // for the result - double c3[8][2] __attribute__((aligned(32))); // for the result - - double* a2 = - (double*)spqlios_alloc_custom_align(32, nn / 2 * 2 * sizeof(double)); // for storing the coefs in reim layout - double* b2 = - (double*)spqlios_alloc_custom_align(32, nn / 2 * 2 * sizeof(double)); // for storing the coefs in reim layout - // double* c2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); // for storing the coefs in reim - // layout - - // organise the coefficients in the reim layout - for (uint32_t i = 0; i < nn / 2; i++) { - a2[i] = a1[i][0]; // a1 = a2, b1 = b2 - a2[nn / 2 + i] = a1[i][1]; - b2[i] = b1[i][0]; - b2[nn / 2 + i] = b1[i][1]; - } - - // fft - cplx_fft_simple(8, a1); - reim_fft_simple(8, a2); - - cplx_fft_simple(8, b1); - reim_fft_simple(8, b2); - - cplx_fftvec_mul_simple(8, c1, a1, b1); - reim_fftvec_mul_simple(8, c2, a2, b2); - - cplx_ifft_simple(8, c1); - reim_ifft_simple(8, c2); - - // check base layout and reim layout result in the same values - double d = 0; - for (uint32_t i = 0; i < nn / 2; i++) { - // printf("RE: cplx_result %lf and reim_result %lf \n", c1[i][0], c2[i]); - // printf("IM: cplx_result %lf and reim_result %lf \n", c1[i][1], c2[nn / 2 + i]); - double dre = fabs(c1[i][0] - c2[i]); - double dim = fabs(c1[i][1] - c2[nn / 2 + i]); - if (dre > d) d = dre; - if (dim > d) d = dim; - ASSERT_LE(d, 1e-7); - } - ASSERT_LE(d, 1e-7); - - // check converting back to base layout: - - for (uint32_t i = 0; i < nn / 2; i++) { - c3[i][0] = c2[i]; - c3[i][1] = c2[8 + i]; - } - - d = 0; - for (uint32_t i = 0; i < nn / 2; i++) { - double dre = fabs(c1[i][0] - c3[i][0]); - double dim = fabs(c1[i][1] - c3[i][1]); - if (dre > d) d = dre; - if (dim > d) d = dim; - ASSERT_LE(d, 1e-7); - } - ASSERT_LE(d, 1e-7); - - spqlios_free(a2); - spqlios_free(b2); - // spqlios_free(c2); -} - -TEST(fft, fftvec_convolution_recursiveoverk) { - static const uint64_t nn = 32768; // vary accross (8192, 16384), 32768, 65536 - double* a = (double*)spqlios_alloc_custom_align(32, nn * 8); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn * 8); - double* b = (double*)spqlios_alloc_custom_align(32, nn * 8); - double* dist_vector = (double*)spqlios_alloc_custom_align(32, nn * 8); - - printf("N size: %" PRId64 "\n", nn); - - for (uint32_t k = 14; k <= 24; k++) { // vary k - printf("k size: %" PRId32 "\n", k); - int64_t p = UINT64_C(1) << k; - for (uint32_t i = 0; i < nn; i++) { - a[i] = (rand() % p) - p / 2; - b[i] = (rand() % p) - p / 2; - a2[i] = 0; - } - cplx_fft_simple(nn / 2, a); - cplx_fft_simple(nn / 2, b); - cplx_fftvec_addmul_simple(nn / 2, a2, a, b); - cplx_ifft_simple(nn / 2, a2); - double distance = 0; - for (uint32_t i = 0; i < nn; i++) { - double curdist = fabs(a2[i] / (nn / 2.) - rint(a2[i] / (nn / 2.))); - if (distance < curdist) distance = curdist; - dist_vector[i] = a2[i] / (nn / 2.) - rint(a2[i] / (nn / 2.)); - } - printf("distance: %lf\n", distance); - ASSERT_LE(distance, 0.5); // switch from previous 0.1 to 0.5 per experiment 1 reqs - double mean = 0; - for (uint32_t i = 0; i < nn; i++) { - mean = mean + dist_vector[i]; - } - mean = mean / nn; - double variance = 0; - for (uint32_t i = 0; i < nn; i++) { - variance = variance + pow((mean - dist_vector[i]), 2); - } - double stdev = sqrt(variance / nn); - printf("stdev: %lf\n", stdev); - } - - spqlios_free(a); - spqlios_free(b); - spqlios_free(a2); - spqlios_free(dist_vector); -} - -#ifdef __x86_64__ -TEST(fft, cplx_fft_ref_vs_fft_reim_ref) { - for (uint64_t nn : {16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - CPLX_FFT_PRECOMP* tables = new_cplx_fft_precomp(m, 0); - REIM_FFT_PRECOMP* reimtables = new_reim_fft_precomp(m, 0); - CPLX* a = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - CPLX* a1 = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn / 2; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - } - memcpy(a1, a, nn / 2 * sizeof(CPLX)); - for (uint32_t i = 0; i < nn / 2; i++) { - a2[i] = a[i][0]; - a2[nn / 2 + i] = a[i][1]; - } - cplx_fft_ref(tables, a1); - reim_fft_ref(reimtables, a2); - double d = 0; - for (uint32_t i = 0; i < nn / 2; i++) { - double dre = fabs(a1[i][0] - a2[i]); - double dim = fabs(a1[i][1] - a2[nn / 2 + i]); - if (dre > d) d = dre; - if (dim > d) d = dim; - ASSERT_LE(d, 1e-7); - } - ASSERT_LE(d, 1e-7); - spqlios_free(a); - spqlios_free(a1); - spqlios_free(a2); - delete_cplx_fft_precomp(tables); - delete_reim_fft_precomp(reimtables); - } -} -#endif - -TEST(fft, cplx_ifft_ref_vs_reim_ifft_ref) { - for (uint64_t nn : {16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - CPLX_IFFT_PRECOMP* tables = new_cplx_ifft_precomp(m, 0); - REIM_IFFT_PRECOMP* reimtables = new_reim_ifft_precomp(m, 0); - CPLX* a = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - CPLX* a1 = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn / 2; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - } - memcpy(a1, a, nn / 2 * sizeof(CPLX)); - for (uint32_t i = 0; i < nn / 2; i++) { - a2[i] = a[i][0]; - a2[nn / 2 + i] = a[i][1]; - } - cplx_ifft_ref(tables, a1); - reim_ifft_ref(reimtables, a2); - double d = 0; - for (uint32_t i = 0; i < nn / 2; i++) { - double dre = fabs(a1[i][0] - a2[i]); - double dim = fabs(a1[i][1] - a2[nn / 2 + i]); - if (dre > d) d = dre; - if (dim > d) d = dim; - ASSERT_LE(d, 1e-7); - } - ASSERT_LE(d, 1e-7); - spqlios_free(a); - spqlios_free(a1); - spqlios_free(a2); - delete_cplx_fft_precomp(tables); - delete_reim_fft_precomp(reimtables); - } -} - -#ifdef __x86_64__ -TEST(fft, reim4_vecfft_addmul_fma_vs_ref) { - for (uint64_t nn : {16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - REIM4_FFTVEC_ADDMUL_PRECOMP* tbl = new_reim4_fftvec_addmul_precomp(m); - ASSERT_TRUE(tbl != nullptr); - double* a1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* b1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* b2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn; i++) { - a1[i] = (rand() % p) - p / 2; // between -p/2 and p/2 - b1[i] = (rand() % p) - p / 2; - r1[i] = (rand() % p) - p / 2; - } - memcpy(a2, a1, nn / 2 * sizeof(CPLX)); - memcpy(b2, b1, nn / 2 * sizeof(CPLX)); - memcpy(r2, r1, nn / 2 * sizeof(CPLX)); - reim4_fftvec_addmul_ref(tbl, r1, a1, b1); - reim4_fftvec_addmul_fma(tbl, r2, a2, b2); - double d = 0; - for (uint32_t i = 0; i < nn; i++) { - double di = fabs(r1[i] - r2[i]); - if (di > d) d = di; - ASSERT_LE(d, 1e-8); - } - ASSERT_LE(d, 1e-8); - spqlios_free(a1); - spqlios_free(a2); - spqlios_free(b1); - spqlios_free(b2); - spqlios_free(r1); - spqlios_free(r2); - delete_reim4_fftvec_addmul_precomp(tbl); - } -} -#endif - -#ifdef __x86_64__ -TEST(fft, reim4_vecfft_mul_fma_vs_ref) { - for (uint64_t nn : {16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - REIM4_FFTVEC_MUL_PRECOMP* tbl = new_reim4_fftvec_mul_precomp(m); - double* a1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* b1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* b2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn; i++) { - a1[i] = (rand() % p) - p / 2; // between -p/2 and p/2 - b1[i] = (rand() % p) - p / 2; - r1[i] = (rand() % p) - p / 2; - } - memcpy(a2, a1, nn / 2 * sizeof(CPLX)); - memcpy(b2, b1, nn / 2 * sizeof(CPLX)); - memcpy(r2, r1, nn / 2 * sizeof(CPLX)); - reim4_fftvec_mul_ref(tbl, r1, a1, b1); - reim4_fftvec_mul_fma(tbl, r2, a2, b2); - double d = 0; - for (uint32_t i = 0; i < nn; i++) { - double di = fabs(r1[i] - r2[i]); - if (di > d) d = di; - ASSERT_LE(d, 1e-8); - } - ASSERT_LE(d, 1e-8); - spqlios_free(a1); - spqlios_free(a2); - spqlios_free(b1); - spqlios_free(b2); - spqlios_free(r1); - spqlios_free(r2); - delete_reim_fftvec_mul_precomp(tbl); - } -} -#endif - -#ifdef __x86_64__ -TEST(fft, reim4_from_cplx_fma_vs_ref) { - for (uint64_t nn : {16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - REIM4_FROM_CPLX_PRECOMP* tbl = new_reim4_from_cplx_precomp(m); - double* a1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn; i++) { - a1[i] = (rand() % p) - p / 2; // between -p/2 and p/2 - r1[i] = (rand() % p) - p / 2; - } - memcpy(a2, a1, nn / 2 * sizeof(CPLX)); - memcpy(r2, r1, nn / 2 * sizeof(CPLX)); - reim4_from_cplx_ref(tbl, r1, a1); - reim4_from_cplx_fma(tbl, r2, a2); - double d = 0; - for (uint32_t i = 0; i < nn; i++) { - double di = fabs(r1[i] - r2[i]); - if (di > d) d = di; - ASSERT_LE(d, 1e-8); - } - ASSERT_LE(d, 1e-8); - spqlios_free(a1); - spqlios_free(a2); - spqlios_free(r1); - spqlios_free(r2); - delete_reim4_from_cplx_precomp(tbl); - } -} -#endif - -#ifdef __x86_64__ -TEST(fft, reim4_to_cplx_fma_vs_ref) { - for (uint64_t nn : {16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - REIM4_TO_CPLX_PRECOMP* tbl = new_reim4_to_cplx_precomp(m); - double* a1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn; i++) { - a1[i] = (rand() % p) - p / 2; // between -p/2 and p/2 - r1[i] = (rand() % p) - p / 2; - } - memcpy(a2, a1, nn / 2 * sizeof(CPLX)); - memcpy(r2, r1, nn / 2 * sizeof(CPLX)); - reim4_to_cplx_ref(tbl, r1, a1); - reim4_to_cplx_fma(tbl, r2, a2); - double d = 0; - for (uint32_t i = 0; i < nn; i++) { - double di = fabs(r1[i] - r2[i]); - if (di > d) d = di; - ASSERT_LE(d, 1e-8); - } - ASSERT_LE(d, 1e-8); - spqlios_free(a1); - spqlios_free(a2); - spqlios_free(r1); - spqlios_free(r2); - delete_reim4_from_cplx_precomp(tbl); - } -} -#endif
D spqlios/lib/test/spqlios_vec_rnx_approxdecomp_tnxdbl_test.cpp

@@ -1,42 +0,0 @@

-#include "gtest/gtest.h" -#include "spqlios/arithmetic/vec_rnx_arithmetic_private.h" -#include "testlib/vec_rnx_layout.h" - -static void test_rnx_approxdecomp(RNX_APPROXDECOMP_FROM_TNXDBL_F approxdec) { - for (const uint64_t nn : {2, 4, 8, 32}) { - MOD_RNX* module = new_rnx_module_info(nn, FFT64); - for (const uint64_t ell : {1, 2, 7}) { - for (const uint64_t k : {2, 5}) { - TNXDBL_APPROXDECOMP_GADGET* gadget = new_tnxdbl_approxdecomp_gadget(module, k, ell); - for (const uint64_t res_size : {ell, ell - 1, ell + 1}) { - const uint64_t res_sl = nn + uniform_u64_bits(2); - rnx_vec_f64_layout in(nn, 1, nn); - in.fill_random(3); - rnx_vec_f64_layout out(nn, res_size, res_sl); - approxdec(module, gadget, out.data(), res_size, res_sl, in.data()); - // reconstruct the output - uint64_t msize = std::min(res_size, ell); - double err_bnd = msize == ell ? pow(2., -double(msize * k) - 1) : pow(2., -double(msize * k)); - for (uint64_t j = 0; j < nn; ++j) { - double in_j = in.data()[j]; - double out_j = 0; - for (uint64_t i = 0; i < res_size; ++i) { - out_j += out.get_copy(i).get_coeff(j) * pow(2., -double((i + 1) * k)); - } - double err = out_j - in_j; - double err_abs = fabs(err - rint(err)); - ASSERT_LE(err_abs, err_bnd); - } - } - delete_tnxdbl_approxdecomp_gadget(gadget); - } - } - delete_rnx_module_info(module); - } -} - -TEST(vec_rnx, rnx_approxdecomp) { test_rnx_approxdecomp(rnx_approxdecomp_from_tnxdbl); } -TEST(vec_rnx, rnx_approxdecomp_ref) { test_rnx_approxdecomp(rnx_approxdecomp_from_tnxdbl_ref); } -#ifdef __x86_64__ -TEST(vec_rnx, rnx_approxdecomp_avx) { test_rnx_approxdecomp(rnx_approxdecomp_from_tnxdbl_avx); } -#endif
D spqlios/lib/test/spqlios_vec_rnx_conversions_test.cpp

@@ -1,134 +0,0 @@

-#include <gtest/gtest.h> -#include <spqlios/arithmetic/vec_rnx_arithmetic_private.h> - -#include "testlib/test_commons.h" - -template <typename SRC_T, typename DST_T> -static void test_conv(void (*conv_f)(const MOD_RNX*, // - DST_T* res, uint64_t res_size, uint64_t res_sl, // - const SRC_T* a, uint64_t a_size, uint64_t a_sl), // - DST_T (*ideal_conv_f)(SRC_T x), // - SRC_T (*random_f)() // -) { - for (uint64_t nn : {2, 4, 16, 64}) { - MOD_RNX* module = new_rnx_module_info(nn, FFT64); - for (uint64_t a_size : {0, 1, 2, 5}) { - for (uint64_t res_size : {0, 1, 3, 5}) { - for (uint64_t trials = 0; trials < 20; ++trials) { - uint64_t a_sl = nn + uniform_u64_bits(2); - uint64_t res_sl = nn + uniform_u64_bits(2); - std::vector<SRC_T> a(a_sl * a_size); - std::vector<DST_T> res(res_sl * res_size); - uint64_t msize = std::min(a_size, res_size); - for (uint64_t i = 0; i < a_size; ++i) { - for (uint64_t j = 0; j < nn; ++j) { - a[i * a_sl + j] = random_f(); - } - } - conv_f(module, res.data(), res_size, res_sl, a.data(), a_size, a_sl); - for (uint64_t i = 0; i < msize; ++i) { - for (uint64_t j = 0; j < nn; ++j) { - SRC_T aij = a[i * a_sl + j]; - DST_T expect = ideal_conv_f(aij); - DST_T actual = res[i * res_sl + j]; - ASSERT_EQ(expect, actual); - } - } - for (uint64_t i = msize; i < res_size; ++i) { - DST_T expect = 0; - for (uint64_t j = 0; j < nn; ++j) { - SRC_T actual = res[i * res_sl + j]; - ASSERT_EQ(expect, actual); - } - } - } - } - } - delete_rnx_module_info(module); - } -} - -static int32_t ideal_dbl_to_tn32(double a) { - double _2p32 = INT64_C(1) << 32; - double a_mod_1 = a - rint(a); - int64_t t = rint(a_mod_1 * _2p32); - return int32_t(t); -} - -static double random_f64_10() { return uniform_f64_bounds(-10, 10); } - -static void test_vec_rnx_to_tnx32(VEC_RNX_TO_TNX32_F vec_rnx_to_tnx32_f) { - test_conv(vec_rnx_to_tnx32_f, ideal_dbl_to_tn32, random_f64_10); -} - -TEST(vec_rnx_arithmetic, vec_rnx_to_tnx32) { test_vec_rnx_to_tnx32(vec_rnx_to_tnx32); } -TEST(vec_rnx_arithmetic, vec_rnx_to_tnx32_ref) { test_vec_rnx_to_tnx32(vec_rnx_to_tnx32_ref); } - -static double ideal_tn32_to_dbl(int32_t a) { - const double _2p32 = INT64_C(1) << 32; - return double(a) / _2p32; -} - -static int32_t random_t32() { return uniform_i64_bits(32); } - -static void test_vec_rnx_from_tnx32(VEC_RNX_FROM_TNX32_F vec_rnx_from_tnx32_f) { - test_conv(vec_rnx_from_tnx32_f, ideal_tn32_to_dbl, random_t32); -} - -TEST(vec_rnx_arithmetic, vec_rnx_from_tnx32) { test_vec_rnx_from_tnx32(vec_rnx_from_tnx32); } -TEST(vec_rnx_arithmetic, vec_rnx_from_tnx32_ref) { test_vec_rnx_from_tnx32(vec_rnx_from_tnx32_ref); } - -static int32_t ideal_dbl_round_to_i32(double a) { return int32_t(rint(a)); } - -static double random_dbl_explaw_18() { return uniform_f64_bounds(-1., 1.) * pow(2., uniform_u64_bits(6) % 19); } - -static void test_vec_rnx_to_znx32(VEC_RNX_TO_ZNX32_F vec_rnx_to_znx32_f) { - test_conv(vec_rnx_to_znx32_f, ideal_dbl_round_to_i32, random_dbl_explaw_18); -} - -TEST(zn_arithmetic, vec_rnx_to_znx32) { test_vec_rnx_to_znx32(vec_rnx_to_znx32); } -TEST(zn_arithmetic, vec_rnx_to_znx32_ref) { test_vec_rnx_to_znx32(vec_rnx_to_znx32_ref); } - -static double ideal_i32_to_dbl(int32_t a) { return double(a); } - -static int32_t random_i32_explaw_18() { return uniform_i64_bits(uniform_u64_bits(6) % 19); } - -static void test_vec_rnx_from_znx32(VEC_RNX_FROM_ZNX32_F vec_rnx_from_znx32_f) { - test_conv(vec_rnx_from_znx32_f, ideal_i32_to_dbl, random_i32_explaw_18); -} - -TEST(zn_arithmetic, vec_rnx_from_znx32) { test_vec_rnx_from_znx32(vec_rnx_from_znx32); } -TEST(zn_arithmetic, vec_rnx_from_znx32_ref) { test_vec_rnx_from_znx32(vec_rnx_from_znx32_ref); } - -static double ideal_dbl_to_tndbl(double a) { return a - rint(a); } - -static void test_vec_rnx_to_tnxdbl(VEC_RNX_TO_TNXDBL_F vec_rnx_to_tnxdbl_f) { - test_conv(vec_rnx_to_tnxdbl_f, ideal_dbl_to_tndbl, random_f64_10); -} - -TEST(zn_arithmetic, vec_rnx_to_tnxdbl) { test_vec_rnx_to_tnxdbl(vec_rnx_to_tnxdbl); } -TEST(zn_arithmetic, vec_rnx_to_tnxdbl_ref) { test_vec_rnx_to_tnxdbl(vec_rnx_to_tnxdbl_ref); } - -#if 0 -static int64_t ideal_dbl_round_to_i64(double a) { return rint(a); } - -static double random_dbl_explaw_50() { return uniform_f64_bounds(-1., 1.) * pow(2., uniform_u64_bits(7) % 51); } - -static void test_dbl_round_to_i64(DBL_ROUND_TO_I64_F dbl_round_to_i64_f) { - test_conv(dbl_round_to_i64_f, ideal_dbl_round_to_i64, random_dbl_explaw_50); -} - -TEST(zn_arithmetic, dbl_round_to_i64) { test_dbl_round_to_i64(dbl_round_to_i64); } -TEST(zn_arithmetic, dbl_round_to_i64_ref) { test_dbl_round_to_i64(dbl_round_to_i64_ref); } - -static double ideal_i64_to_dbl(int64_t a) { return double(a); } - -static int64_t random_i64_explaw_50() { return uniform_i64_bits(uniform_u64_bits(7) % 51); } - -static void test_i64_to_dbl(I64_TO_DBL_F i64_to_dbl_f) { - test_conv(i64_to_dbl_f, ideal_i64_to_dbl, random_i64_explaw_50); -} - -TEST(zn_arithmetic, i64_to_dbl) { test_i64_to_dbl(i64_to_dbl); } -TEST(zn_arithmetic, i64_to_dbl_ref) { test_i64_to_dbl(i64_to_dbl_ref); } -#endif
D spqlios/lib/test/spqlios_vec_rnx_ppol_test.cpp

@@ -1,73 +0,0 @@

-#include <gtest/gtest.h> - -#include "spqlios/arithmetic/vec_rnx_arithmetic_private.h" -#include "spqlios/reim/reim_fft.h" -#include "test/testlib/vec_rnx_layout.h" - -static void test_vec_rnx_svp_prepare(RNX_SVP_PREPARE_F* rnx_svp_prepare, BYTES_OF_RNX_SVP_PPOL_F* tmp_bytes) { - for (uint64_t n : {2, 4, 8, 64}) { - MOD_RNX* mod = new_rnx_module_info(n, FFT64); - const double invm = 1. / mod->m; - - rnx_f64 in = rnx_f64::random_log2bound(n, 40); - rnx_f64 in_divide_by_m = rnx_f64::zero(n); - for (uint64_t i = 0; i < n; ++i) { - in_divide_by_m.set_coeff(i, in.get_coeff(i) * invm); - } - fft64_rnx_svp_ppol_layout out(n); - reim_fft64vec expect = simple_fft64(in_divide_by_m); - rnx_svp_prepare(mod, out.data, in.data()); - const double* ed = (double*)expect.data(); - const double* ac = (double*)out.data; - for (uint64_t i = 0; i < n; ++i) { - ASSERT_LE(abs(ed[i] - ac[i]), 1e-10) << i << n; - } - delete_rnx_module_info(mod); - } -} -TEST(vec_rnx, vec_rnx_svp_prepare) { test_vec_rnx_svp_prepare(rnx_svp_prepare, bytes_of_rnx_svp_ppol); } -TEST(vec_rnx, vec_rnx_svp_prepare_ref) { - test_vec_rnx_svp_prepare(fft64_rnx_svp_prepare_ref, fft64_bytes_of_rnx_svp_ppol); -} - -static void test_vec_rnx_svp_apply(RNX_SVP_APPLY_F* apply) { - for (uint64_t n : {2, 4, 8, 64, 128}) { - MOD_RNX* mod = new_rnx_module_info(n, FFT64); - - // poly 1 to multiply - create and prepare - fft64_rnx_svp_ppol_layout ppol(n); - ppol.fill_random(1.); - for (uint64_t sa : {3, 5, 8}) { - for (uint64_t sr : {3, 5, 8}) { - uint64_t a_sl = n + uniform_u64_bits(2); - uint64_t r_sl = n + uniform_u64_bits(2); - // poly 2 to multiply - rnx_vec_f64_layout a(n, sa, a_sl); - a.fill_random(19); - - // original operation result - rnx_vec_f64_layout res(n, sr, r_sl); - thash hash_a_before = a.content_hash(); - thash hash_ppol_before = ppol.content_hash(); - apply(mod, res.data(), sr, r_sl, ppol.data, a.data(), sa, a_sl); - ASSERT_EQ(a.content_hash(), hash_a_before); - ASSERT_EQ(ppol.content_hash(), hash_ppol_before); - // create expected value - reim_fft64vec ppo = ppol.get_copy(); - std::vector<rnx_f64> expect(sr); - for (uint64_t i = 0; i < sr; ++i) { - expect[i] = simple_ifft64(ppo * simple_fft64(a.get_copy_zext(i))); - } - // this is the largest precision we can safely expect - double prec_expect = n * pow(2., 19 - 50); - for (uint64_t i = 0; i < sr; ++i) { - rnx_f64 actual = res.get_copy_zext(i); - ASSERT_LE(infty_dist(actual, expect[i]), prec_expect); - } - } - } - delete_rnx_module_info(mod); - } -} -TEST(vec_rnx, vec_rnx_svp_apply) { test_vec_rnx_svp_apply(rnx_svp_apply); } -TEST(vec_rnx, vec_rnx_svp_apply_ref) { test_vec_rnx_svp_apply(fft64_rnx_svp_apply_ref); }
D spqlios/lib/test/spqlios_vec_rnx_test.cpp

@@ -1,417 +0,0 @@

-#include <gtest/gtest.h> - -#include "spqlios/arithmetic/vec_rnx_arithmetic_private.h" -#include "spqlios/reim/reim_fft.h" -#include "testlib/vec_rnx_layout.h" - -// disabling this test by default, since it depicts on purpose wrong accesses -#if 0 -TEST(rnx_layout, valgrind_antipattern_test) { - uint64_t n = 4; - rnx_vec_f64_layout v(n, 7, 13); - // this should be ok - v.set(0, rnx_f64::zero(n)); - // this should abort (wrong ring dimension) - ASSERT_DEATH(v.set(3, rnx_f64::zero(2 * n)), ""); - // this should abort (out of bounds) - ASSERT_DEATH(v.set(8, rnx_f64::zero(n)), ""); - // this should be ok - ASSERT_EQ(v.get_copy_zext(0), rnx_f64::zero(n)); - // should be an uninit read - ASSERT_TRUE(!(v.get_copy_zext(2) == rnx_f64::zero(n))); // should be uninit - // should be an invalid read (inter-slice) - ASSERT_NE(v.data()[4], 0); - ASSERT_EQ(v.data()[2], 0); // should be ok - // should be an uninit read - ASSERT_NE(v.data()[13], 0); // should be uninit -} -#endif - -// test of binary operations - -// test for out of place calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_binop_outplace(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - for (uint64_t n : {2, 4, 8, 128}) { - RNX_MODULE_TYPE mtype = FFT64; - MOD_RNX* mod = new_rnx_module_info(n, mtype); - for (uint64_t sa : {7, 13, 15}) { - for (uint64_t sb : {7, 13, 15}) { - for (uint64_t sc : {7, 13, 15}) { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - uint64_t b_sl = uniform_u64_bits(3) * 5 + n; - uint64_t c_sl = uniform_u64_bits(3) * 5 + n; - rnx_vec_f64_layout la(n, sa, a_sl); - rnx_vec_f64_layout lb(n, sb, b_sl); - rnx_vec_f64_layout lc(n, sc, c_sl); - std::vector<rnx_f64> expect(sc); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, rnx_f64::random_log2bound(n, 1.)); - } - for (uint64_t i = 0; i < sb; ++i) { - lb.set(i, rnx_f64::random_log2bound(n, 1.)); - } - for (uint64_t i = 0; i < sc; ++i) { - expect[i] = ref_binop(la.get_copy_zext(i), lb.get_copy_zext(i)); - } - binop(mod, // N - lc.data(), sc, c_sl, // res - la.data(), sa, a_sl, // a - lb.data(), sb, b_sl); - for (uint64_t i = 0; i < sc; ++i) { - ASSERT_EQ(lc.get_copy_zext(i), expect[i]); - } - } - } - } - delete_rnx_module_info(mod); - } -} -// test for inplace1 calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_binop_inplace1(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - for (uint64_t n : {2, 4, 64}) { - RNX_MODULE_TYPE mtype = FFT64; - MOD_RNX* mod = new_rnx_module_info(n, mtype); - for (uint64_t sa : {3, 9, 12}) { - for (uint64_t sb : {3, 9, 12}) { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - uint64_t b_sl = uniform_u64_bits(3) * 5 + n; - rnx_vec_f64_layout la(n, sa, a_sl); - rnx_vec_f64_layout lb(n, sb, b_sl); - std::vector<rnx_f64> expect(sa); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, rnx_f64::random_log2bound(n, 1.)); - } - for (uint64_t i = 0; i < sb; ++i) { - lb.set(i, rnx_f64::random_log2bound(n, 1.)); - } - for (uint64_t i = 0; i < sa; ++i) { - expect[i] = ref_binop(la.get_copy_zext(i), lb.get_copy_zext(i)); - } - binop(mod, // N - la.data(), sa, a_sl, // res - la.data(), sa, a_sl, // a - lb.data(), sb, b_sl); - for (uint64_t i = 0; i < sa; ++i) { - ASSERT_EQ(la.get_copy_zext(i), expect[i]); - } - } - } - delete_rnx_module_info(mod); - } -} -// test for inplace2 calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_binop_inplace2(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - for (uint64_t n : {4, 32, 64}) { - RNX_MODULE_TYPE mtype = FFT64; - MOD_RNX* mod = new_rnx_module_info(n, mtype); - for (uint64_t sa : {3, 9, 12}) { - for (uint64_t sb : {3, 9, 12}) { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - uint64_t b_sl = uniform_u64_bits(3) * 5 + n; - rnx_vec_f64_layout la(n, sa, a_sl); - rnx_vec_f64_layout lb(n, sb, b_sl); - std::vector<rnx_f64> expect(sb); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, rnx_f64::random_log2bound(n, 1.)); - } - for (uint64_t i = 0; i < sb; ++i) { - lb.set(i, rnx_f64::random_log2bound(n, 1.)); - } - for (uint64_t i = 0; i < sb; ++i) { - expect[i] = ref_binop(la.get_copy_zext(i), lb.get_copy_zext(i)); - } - binop(mod, // N - lb.data(), sb, b_sl, // res - la.data(), sa, a_sl, // a - lb.data(), sb, b_sl); - for (uint64_t i = 0; i < sb; ++i) { - ASSERT_EQ(lb.get_copy_zext(i), expect[i]); - } - } - } - delete_rnx_module_info(mod); - } -} -// test for inplace3 calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_binop_inplace3(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - for (uint64_t n : {2, 16, 1024}) { - RNX_MODULE_TYPE mtype = FFT64; - MOD_RNX* mod = new_rnx_module_info(n, mtype); - for (uint64_t sa : {2, 6, 11}) { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - rnx_vec_f64_layout la(n, sa, a_sl); - std::vector<rnx_f64> expect(sa); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, rnx_f64::random_log2bound(n, 1.)); - } - for (uint64_t i = 0; i < sa; ++i) { - expect[i] = ref_binop(la.get_copy_zext(i), la.get_copy_zext(i)); - } - binop(mod, // N - la.data(), sa, a_sl, // res - la.data(), sa, a_sl, // a - la.data(), sa, a_sl); - for (uint64_t i = 0; i < sa; ++i) { - ASSERT_EQ(la.get_copy_zext(i), expect[i]); - } - } - delete_rnx_module_info(mod); - } -} -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_binop(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - test_vec_rnx_elemw_binop_outplace(binop, ref_binop); - test_vec_rnx_elemw_binop_inplace1(binop, ref_binop); - test_vec_rnx_elemw_binop_inplace2(binop, ref_binop); - test_vec_rnx_elemw_binop_inplace3(binop, ref_binop); -} - -static rnx_f64 poly_add(const rnx_f64& a, const rnx_f64& b) { return a + b; } -static rnx_f64 poly_sub(const rnx_f64& a, const rnx_f64& b) { return a - b; } -TEST(vec_rnx, vec_rnx_add) { test_vec_rnx_elemw_binop(vec_rnx_add, poly_add); } -TEST(vec_rnx, vec_rnx_add_ref) { test_vec_rnx_elemw_binop(vec_rnx_add_ref, poly_add); } -#ifdef __x86_64__ -TEST(vec_rnx, vec_rnx_add_avx) { test_vec_rnx_elemw_binop(vec_rnx_add_avx, poly_add); } -#endif -TEST(vec_rnx, vec_rnx_sub) { test_vec_rnx_elemw_binop(vec_rnx_sub, poly_sub); } -TEST(vec_rnx, vec_rnx_sub_ref) { test_vec_rnx_elemw_binop(vec_rnx_sub_ref, poly_sub); } -#ifdef __x86_64__ -TEST(vec_rnx, vec_rnx_sub_avx) { test_vec_rnx_elemw_binop(vec_rnx_sub_avx, poly_sub); } -#endif - -// test for out of place calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_unop_param_outplace(ACTUAL_FCN test_mul_xp_minus_one, EXPECT_FCN ref_mul_xp_minus_one, - int64_t (*param_gen)()) { - for (uint64_t n : {2, 4, 8, 128}) { - MOD_RNX* mod = new_rnx_module_info(n, FFT64); - for (uint64_t sa : {7, 13, 15}) { - for (uint64_t sb : {7, 13, 15}) { - { - int64_t p = param_gen(); - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - uint64_t b_sl = uniform_u64_bits(3) * 4 + n; - rnx_vec_f64_layout la(n, sa, a_sl); - rnx_vec_f64_layout lb(n, sb, b_sl); - std::vector<rnx_f64> expect(sb); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, rnx_f64::random_log2bound(n, 0)); - } - for (uint64_t i = 0; i < sb; ++i) { - expect[i] = ref_mul_xp_minus_one(p, la.get_copy_zext(i)); - } - test_mul_xp_minus_one(mod, // - p, // - lb.data(), sb, b_sl, // - la.data(), sa, a_sl // - ); - for (uint64_t i = 0; i < sb; ++i) { - ASSERT_EQ(lb.get_copy_zext(i), expect[i]) << n << " " << sa << " " << sb << " " << i; - } - } - } - } - delete_rnx_module_info(mod); - } -} - -// test for inplace calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_unop_param_inplace(ACTUAL_FCN actual_function, EXPECT_FCN ref_function, - int64_t (*param_gen)()) { - for (uint64_t n : {2, 16, 1024}) { - MOD_RNX* mod = new_rnx_module_info(n, FFT64); - for (uint64_t sa : {2, 6, 11}) { - { - int64_t p = param_gen(); - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - rnx_vec_f64_layout la(n, sa, a_sl); - std::vector<rnx_f64> expect(sa); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, rnx_f64::random_log2bound(n, 0)); - } - for (uint64_t i = 0; i < sa; ++i) { - expect[i] = ref_function(p, la.get_copy_zext(i)); - } - actual_function(mod, // N - p, //; - la.data(), sa, a_sl, // res - la.data(), sa, a_sl // a - ); - for (uint64_t i = 0; i < sa; ++i) { - ASSERT_EQ(la.get_copy_zext(i), expect[i]) << n << " " << sa << " " << i; - } - } - } - delete_rnx_module_info(mod); - } -} - -static int64_t random_mul_xp_minus_one_param() { return uniform_i64(); } -static int64_t random_automorphism_param() { return 2 * uniform_i64() + 1; } -static int64_t random_rotation_param() { return uniform_i64(); } - -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_mul_xp_minus_one(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - test_vec_rnx_elemw_unop_param_outplace(binop, ref_binop, random_mul_xp_minus_one_param); - test_vec_rnx_elemw_unop_param_inplace(binop, ref_binop, random_mul_xp_minus_one_param); -} -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_rotate(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - test_vec_rnx_elemw_unop_param_outplace(binop, ref_binop, random_rotation_param); - test_vec_rnx_elemw_unop_param_inplace(binop, ref_binop, random_rotation_param); -} -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_automorphism(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - test_vec_rnx_elemw_unop_param_outplace(binop, ref_binop, random_automorphism_param); - test_vec_rnx_elemw_unop_param_inplace(binop, ref_binop, random_automorphism_param); -} - -static rnx_f64 poly_mul_xp_minus_one(const int64_t p, const rnx_f64& a) { - uint64_t n = a.nn(); - rnx_f64 res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, a.get_coeff(i - p) - a.get_coeff(i)); - } - return res; -} -static rnx_f64 poly_rotate(const int64_t p, const rnx_f64& a) { - uint64_t n = a.nn(); - rnx_f64 res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, a.get_coeff(i - p)); - } - return res; -} -static rnx_f64 poly_automorphism(const int64_t p, const rnx_f64& a) { - uint64_t n = a.nn(); - rnx_f64 res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i * p, a.get_coeff(i)); - } - return res; -} - -TEST(vec_rnx, vec_rnx_mul_xp_minus_one) { - test_vec_rnx_elemw_mul_xp_minus_one(vec_rnx_mul_xp_minus_one, poly_mul_xp_minus_one); -} -TEST(vec_rnx, vec_rnx_mul_xp_minus_one_ref) { - test_vec_rnx_elemw_mul_xp_minus_one(vec_rnx_mul_xp_minus_one_ref, poly_mul_xp_minus_one); -} - -TEST(vec_rnx, vec_rnx_rotate) { test_vec_rnx_elemw_rotate(vec_rnx_rotate, poly_rotate); } -TEST(vec_rnx, vec_rnx_rotate_ref) { test_vec_rnx_elemw_rotate(vec_rnx_rotate_ref, poly_rotate); } -TEST(vec_rnx, vec_rnx_automorphism) { test_vec_rnx_elemw_automorphism(vec_rnx_automorphism, poly_automorphism); } -TEST(vec_rnx, vec_rnx_automorphism_ref) { - test_vec_rnx_elemw_automorphism(vec_rnx_automorphism_ref, poly_automorphism); -} - -// test for out of place calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_unop_outplace(ACTUAL_FCN actual_function, EXPECT_FCN ref_function) { - for (uint64_t n : {2, 4, 8, 128}) { - MOD_RNX* mod = new_rnx_module_info(n, FFT64); - for (uint64_t sa : {7, 13, 15}) { - for (uint64_t sb : {7, 13, 15}) { - { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - uint64_t b_sl = uniform_u64_bits(3) * 4 + n; - rnx_vec_f64_layout la(n, sa, a_sl); - rnx_vec_f64_layout lb(n, sb, b_sl); - std::vector<rnx_f64> expect(sb); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, rnx_f64::random_log2bound(n, 0)); - } - for (uint64_t i = 0; i < sb; ++i) { - expect[i] = ref_function(la.get_copy_zext(i)); - } - actual_function(mod, // - lb.data(), sb, b_sl, // - la.data(), sa, a_sl // - ); - for (uint64_t i = 0; i < sb; ++i) { - ASSERT_EQ(lb.get_copy_zext(i), expect[i]) << n << " " << sa << " " << sb << " " << i; - } - } - } - } - delete_rnx_module_info(mod); - } -} - -// test for inplace calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_unop_inplace(ACTUAL_FCN actual_function, EXPECT_FCN ref_function) { - for (uint64_t n : {2, 16, 1024}) { - MOD_RNX* mod = new_rnx_module_info(n, FFT64); - for (uint64_t sa : {2, 6, 11}) { - { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - rnx_vec_f64_layout la(n, sa, a_sl); - std::vector<rnx_f64> expect(sa); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, rnx_f64::random_log2bound(n, 0)); - } - for (uint64_t i = 0; i < sa; ++i) { - expect[i] = ref_function(la.get_copy_zext(i)); - } - actual_function(mod, // N - la.data(), sa, a_sl, // res - la.data(), sa, a_sl // a - ); - for (uint64_t i = 0; i < sa; ++i) { - ASSERT_EQ(la.get_copy_zext(i), expect[i]) << n << " " << sa << " " << i; - } - } - } - delete_rnx_module_info(mod); - } -} -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_unop(ACTUAL_FCN unnop, EXPECT_FCN ref_unnop) { - test_vec_rnx_elemw_unop_outplace(unnop, ref_unnop); - test_vec_rnx_elemw_unop_inplace(unnop, ref_unnop); -} - -static rnx_f64 poly_copy(const rnx_f64& a) { return a; } -static rnx_f64 poly_negate(const rnx_f64& a) { return -a; } - -TEST(vec_rnx, vec_rnx_copy) { test_vec_rnx_elemw_unop(vec_rnx_copy, poly_copy); } -TEST(vec_rnx, vec_rnx_copy_ref) { test_vec_rnx_elemw_unop(vec_rnx_copy_ref, poly_copy); } -TEST(vec_rnx, vec_rnx_negate) { test_vec_rnx_elemw_unop(vec_rnx_negate, poly_negate); } -TEST(vec_rnx, vec_rnx_negate_ref) { test_vec_rnx_elemw_unop(vec_rnx_negate_ref, poly_negate); } -#ifdef __x86_64__ -TEST(vec_rnx, vec_rnx_negate_avx) { test_vec_rnx_elemw_unop(vec_rnx_negate_avx, poly_negate); } -#endif - -// test for inplace calls -void test_vec_rnx_zero(VEC_RNX_ZERO_F actual_function) { - for (uint64_t n : {2, 16, 1024}) { - MOD_RNX* mod = new_rnx_module_info(n, FFT64); - for (uint64_t sa : {2, 6, 11}) { - { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - rnx_vec_f64_layout la(n, sa, a_sl); - const rnx_f64 ZERO = rnx_f64::zero(n); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, rnx_f64::random_log2bound(n, 0)); - } - actual_function(mod, // N - la.data(), sa, a_sl // res - ); - for (uint64_t i = 0; i < sa; ++i) { - ASSERT_EQ(la.get_copy_zext(i), ZERO) << n << " " << sa << " " << i; - } - } - } - delete_rnx_module_info(mod); - } -} - -TEST(vec_rnx, vec_rnx_zero) { test_vec_rnx_zero(vec_rnx_zero); } - -TEST(vec_rnx, vec_rnx_zero_ref) { test_vec_rnx_zero(vec_rnx_zero_ref); }
D spqlios/lib/test/spqlios_vec_rnx_vmp_test.cpp

@@ -1,291 +0,0 @@

-#include "gtest/gtest.h" -#include "../spqlios/arithmetic/vec_rnx_arithmetic_private.h" -#include "../spqlios/reim/reim_fft.h" -#include "testlib/vec_rnx_layout.h" - -static void test_vmp_apply_dft_to_dft_outplace( // - RNX_VMP_APPLY_DFT_TO_DFT_F* apply, // - RNX_VMP_APPLY_DFT_TO_DFT_TMP_BYTES_F* tmp_bytes) { - for (uint64_t nn : {2, 4, 8, 64}) { - MOD_RNX* module = new_rnx_module_info(nn, FFT64); - for (uint64_t mat_nrows : {1, 4, 7}) { - for (uint64_t mat_ncols : {1, 2, 5}) { - for (uint64_t in_size : {1, 4, 7}) { - for (uint64_t out_size : {1, 2, 5}) { - const uint64_t in_sl = nn + uniform_u64_bits(2); - const uint64_t out_sl = nn + uniform_u64_bits(2); - rnx_vec_f64_layout in(nn, in_size, in_sl); - fft64_rnx_vmp_pmat_layout pmat(nn, mat_nrows, mat_ncols); - rnx_vec_f64_layout out(nn, out_size, out_sl); - in.fill_random(0); - pmat.fill_random(0); - // naive computation of the product - std::vector<reim_fft64vec> expect(out_size, reim_fft64vec(nn)); - for (uint64_t col = 0; col < out_size; ++col) { - reim_fft64vec ex = reim_fft64vec::zero(nn); - for (uint64_t row = 0; row < std::min(mat_nrows, in_size); ++row) { - ex += pmat.get_zext(row, col) * in.get_dft_copy(row); - } - expect[col] = ex; - } - // apply the product - std::vector<uint8_t> tmp(tmp_bytes(module, out_size, in_size, mat_nrows, mat_ncols)); - apply(module, // - out.data(), out_size, out_sl, // - in.data(), in_size, in_sl, // - pmat.data, mat_nrows, mat_ncols, // - tmp.data()); - // check that the output is close from the expectation - for (uint64_t col = 0; col < out_size; ++col) { - reim_fft64vec actual = out.get_dft_copy_zext(col); - ASSERT_LE(infty_dist(actual, expect[col]), 1e-10); - } - } - } - } - } - delete_rnx_module_info(module); - } -} - -static void test_vmp_apply_dft_to_dft_inplace( // - RNX_VMP_APPLY_DFT_TO_DFT_F* apply, // - RNX_VMP_APPLY_DFT_TO_DFT_TMP_BYTES_F* tmp_bytes) { - for (uint64_t nn : {2, 4, 8, 64}) { - MOD_RNX* module = new_rnx_module_info(nn, FFT64); - for (uint64_t mat_nrows : {1, 2, 6}) { - for (uint64_t mat_ncols : {1, 2, 7, 8}) { - for (uint64_t in_size : {1, 3, 6}) { - for (uint64_t out_size : {1, 3, 6}) { - const uint64_t in_out_sl = nn + uniform_u64_bits(2); - rnx_vec_f64_layout in_out(nn, std::max(in_size, out_size), in_out_sl); - fft64_rnx_vmp_pmat_layout pmat(nn, mat_nrows, mat_ncols); - in_out.fill_random(0); - pmat.fill_random(0); - // naive computation of the product - std::vector<reim_fft64vec> expect(out_size, reim_fft64vec(nn)); - for (uint64_t col = 0; col < out_size; ++col) { - reim_fft64vec ex = reim_fft64vec::zero(nn); - for (uint64_t row = 0; row < std::min(mat_nrows, in_size); ++row) { - ex += pmat.get_zext(row, col) * in_out.get_dft_copy(row); - } - expect[col] = ex; - } - // apply the product - std::vector<uint8_t> tmp(tmp_bytes(module, out_size, in_size, mat_nrows, mat_ncols)); - apply(module, // - in_out.data(), out_size, in_out_sl, // - in_out.data(), in_size, in_out_sl, // - pmat.data, mat_nrows, mat_ncols, // - tmp.data()); - // check that the output is close from the expectation - for (uint64_t col = 0; col < out_size; ++col) { - reim_fft64vec actual = in_out.get_dft_copy_zext(col); - ASSERT_LE(infty_dist(actual, expect[col]), 1e-10); - } - } - } - } - } - delete_rnx_module_info(module); - } -} - -static void test_vmp_apply_dft_to_dft( // - RNX_VMP_APPLY_DFT_TO_DFT_F* apply, // - RNX_VMP_APPLY_DFT_TO_DFT_TMP_BYTES_F* tmp_bytes) { - test_vmp_apply_dft_to_dft_outplace(apply, tmp_bytes); - test_vmp_apply_dft_to_dft_inplace(apply, tmp_bytes); -} - -TEST(vec_rnx, vmp_apply_to_dft) { - test_vmp_apply_dft_to_dft(rnx_vmp_apply_dft_to_dft, rnx_vmp_apply_dft_to_dft_tmp_bytes); -} -TEST(vec_rnx, fft64_vmp_apply_dft_to_dft_ref) { - test_vmp_apply_dft_to_dft(fft64_rnx_vmp_apply_dft_to_dft_ref, fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_ref); -} -#ifdef __x86_64__ -TEST(vec_rnx, fft64_vmp_apply_dft_to_dft_avx) { - test_vmp_apply_dft_to_dft(fft64_rnx_vmp_apply_dft_to_dft_avx, fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_avx); -} -#endif - -/// rnx_vmp_prepare - -static void test_vmp_prepare_contiguous(RNX_VMP_PREPARE_CONTIGUOUS_F* prepare_contiguous, - RNX_VMP_PREPARE_CONTIGUOUS_TMP_BYTES_F* tmp_bytes) { - // tests when n < 8 - for (uint64_t nn : {2, 4}) { - const double one_over_m = 2. / nn; - MOD_RNX* module = new_rnx_module_info(nn, FFT64); - for (uint64_t nrows : {1, 2, 5}) { - for (uint64_t ncols : {2, 6, 7}) { - rnx_vec_f64_layout mat(nn, nrows * ncols, nn); - fft64_rnx_vmp_pmat_layout pmat(nn, nrows, ncols); - mat.fill_random(0); - std::vector<uint8_t> tmp_space(tmp_bytes(module)); - thash hash_before = mat.content_hash(); - prepare_contiguous(module, pmat.data, mat.data(), nrows, ncols, tmp_space.data()); - ASSERT_EQ(mat.content_hash(), hash_before); - for (uint64_t row = 0; row < nrows; ++row) { - for (uint64_t col = 0; col < ncols; ++col) { - const double* pmatv = (double*)pmat.data + (col * nrows + row) * nn; - reim_fft64vec tmp = one_over_m * simple_fft64(mat.get_copy(row * ncols + col)); - const double* tmpv = tmp.data(); - for (uint64_t i = 0; i < nn; ++i) { - ASSERT_LE(abs(pmatv[i] - tmpv[i]), 1e-10); - } - } - } - } - } - delete_rnx_module_info(module); - } - // tests when n >= 8 - for (uint64_t nn : {8, 32}) { - const double one_over_m = 2. / nn; - MOD_RNX* module = new_rnx_module_info(nn, FFT64); - uint64_t nblk = nn / 8; - for (uint64_t nrows : {1, 2, 5}) { - for (uint64_t ncols : {2, 6, 7}) { - rnx_vec_f64_layout mat(nn, nrows * ncols, nn); - fft64_rnx_vmp_pmat_layout pmat(nn, nrows, ncols); - mat.fill_random(0); - std::vector<uint8_t> tmp_space(tmp_bytes(module)); - thash hash_before = mat.content_hash(); - prepare_contiguous(module, pmat.data, mat.data(), nrows, ncols, tmp_space.data()); - ASSERT_EQ(mat.content_hash(), hash_before); - for (uint64_t row = 0; row < nrows; ++row) { - for (uint64_t col = 0; col < ncols; ++col) { - reim_fft64vec tmp = one_over_m * simple_fft64(mat.get_copy(row * ncols + col)); - for (uint64_t blk = 0; blk < nblk; ++blk) { - reim4_elem expect = tmp.get_blk(blk); - reim4_elem actual = pmat.get(row, col, blk); - ASSERT_LE(infty_dist(actual, expect), 1e-10); - } - } - } - } - } - delete_rnx_module_info(module); - } -} - -TEST(vec_rnx, vmp_prepare_contiguous) { - test_vmp_prepare_contiguous(rnx_vmp_prepare_contiguous, rnx_vmp_prepare_contiguous_tmp_bytes); -} -TEST(vec_rnx, fft64_vmp_prepare_contiguous_ref) { - test_vmp_prepare_contiguous(fft64_rnx_vmp_prepare_contiguous_ref, fft64_rnx_vmp_prepare_contiguous_tmp_bytes_ref); -} -#ifdef __x86_64__ -TEST(vec_rnx, fft64_vmp_prepare_contiguous_avx) { - test_vmp_prepare_contiguous(fft64_rnx_vmp_prepare_contiguous_avx, fft64_rnx_vmp_prepare_contiguous_tmp_bytes_avx); -} -#endif - -/// rnx_vmp_apply_dft_to_dft - -static void test_vmp_apply_tmp_a_outplace( // - RNX_VMP_APPLY_TMP_A_F* apply, // - RNX_VMP_APPLY_TMP_A_TMP_BYTES_F* tmp_bytes) { - for (uint64_t nn : {2, 4, 8, 64}) { - MOD_RNX* module = new_rnx_module_info(nn, FFT64); - for (uint64_t mat_nrows : {1, 4, 7}) { - for (uint64_t mat_ncols : {1, 2, 5}) { - for (uint64_t in_size : {1, 4, 7}) { - for (uint64_t out_size : {1, 2, 5}) { - const uint64_t in_sl = nn + uniform_u64_bits(2); - const uint64_t out_sl = nn + uniform_u64_bits(2); - rnx_vec_f64_layout in(nn, in_size, in_sl); - fft64_rnx_vmp_pmat_layout pmat(nn, mat_nrows, mat_ncols); - rnx_vec_f64_layout out(nn, out_size, out_sl); - in.fill_random(0); - pmat.fill_random(0); - // naive computation of the product - std::vector<rnx_f64> expect(out_size); - for (uint64_t col = 0; col < out_size; ++col) { - reim_fft64vec ex = reim_fft64vec::zero(nn); - for (uint64_t row = 0; row < std::min(mat_nrows, in_size); ++row) { - ex += pmat.get_zext(row, col) * simple_fft64(in.get_copy(row)); - } - expect[col] = simple_ifft64(ex); - } - // apply the product - std::vector<uint8_t> tmp(tmp_bytes(module, out_size, in_size, mat_nrows, mat_ncols)); - apply(module, // - out.data(), out_size, out_sl, // - in.data(), in_size, in_sl, // - pmat.data, mat_nrows, mat_ncols, // - tmp.data()); - // check that the output is close from the expectation - for (uint64_t col = 0; col < out_size; ++col) { - rnx_f64 actual = out.get_copy_zext(col); - ASSERT_LE(infty_dist(actual, expect[col]), 1e-10); - } - } - } - } - } - delete_rnx_module_info(module); - } -} - -static void test_vmp_apply_tmp_a_inplace( // - RNX_VMP_APPLY_TMP_A_F* apply, // - RNX_VMP_APPLY_TMP_A_TMP_BYTES_F* tmp_bytes) { - for (uint64_t nn : {2, 4, 8, 64}) { - MOD_RNX* module = new_rnx_module_info(nn, FFT64); - for (uint64_t mat_nrows : {1, 4, 7}) { - for (uint64_t mat_ncols : {1, 2, 5}) { - for (uint64_t in_size : {1, 4, 7}) { - for (uint64_t out_size : {1, 2, 5}) { - const uint64_t in_out_sl = nn + uniform_u64_bits(2); - rnx_vec_f64_layout in_out(nn, std::max(in_size, out_size), in_out_sl); - fft64_rnx_vmp_pmat_layout pmat(nn, mat_nrows, mat_ncols); - in_out.fill_random(0); - pmat.fill_random(0); - // naive computation of the product - std::vector<rnx_f64> expect(out_size); - for (uint64_t col = 0; col < out_size; ++col) { - reim_fft64vec ex = reim_fft64vec::zero(nn); - for (uint64_t row = 0; row < std::min(mat_nrows, in_size); ++row) { - ex += pmat.get_zext(row, col) * simple_fft64(in_out.get_copy(row)); - } - expect[col] = simple_ifft64(ex); - } - // apply the product - std::vector<uint8_t> tmp(tmp_bytes(module, out_size, in_size, mat_nrows, mat_ncols)); - apply(module, // - in_out.data(), out_size, in_out_sl, // - in_out.data(), in_size, in_out_sl, // - pmat.data, mat_nrows, mat_ncols, // - tmp.data()); - // check that the output is close from the expectation - for (uint64_t col = 0; col < out_size; ++col) { - rnx_f64 actual = in_out.get_copy_zext(col); - ASSERT_LE(infty_dist(actual, expect[col]), 1e-10); - } - } - } - } - } - delete_rnx_module_info(module); - } -} - -static void test_vmp_apply_tmp_a( // - RNX_VMP_APPLY_TMP_A_F* apply, // - RNX_VMP_APPLY_TMP_A_TMP_BYTES_F* tmp_bytes) { - test_vmp_apply_tmp_a_outplace(apply, tmp_bytes); - test_vmp_apply_tmp_a_inplace(apply, tmp_bytes); -} - -TEST(vec_znx, fft64_vmp_apply_tmp_a) { test_vmp_apply_tmp_a(rnx_vmp_apply_tmp_a, rnx_vmp_apply_tmp_a_tmp_bytes); } -TEST(vec_znx, fft64_vmp_apply_tmp_a_ref) { - test_vmp_apply_tmp_a(fft64_rnx_vmp_apply_tmp_a_ref, fft64_rnx_vmp_apply_tmp_a_tmp_bytes_ref); -} -#ifdef __x86_64__ -TEST(vec_znx, fft64_vmp_apply_tmp_a_avx) { - test_vmp_apply_tmp_a(fft64_rnx_vmp_apply_tmp_a_avx, fft64_rnx_vmp_apply_tmp_a_tmp_bytes_avx); -} -#endif
D spqlios/lib/test/spqlios_vec_znx_big_test.cpp

@@ -1,265 +0,0 @@

-#include <gtest/gtest.h> - -#include "spqlios/arithmetic/vec_znx_arithmetic_private.h" -#include "test/testlib/polynomial_vector.h" -#include "testlib/fft64_layouts.h" -#include "testlib/test_commons.h" - -#define def_rand_big(varname, ringdim, varsize) \ - fft64_vec_znx_big_layout varname(ringdim, varsize); \ - varname.fill_random() - -#define def_rand_small(varname, ringdim, varsize) \ - znx_vec_i64_layout varname(ringdim, varsize, 2 * ringdim); \ - varname.fill_random() - -#define test_prelude(ringdim, moduletype, dim1, dim2, dim3) \ - uint64_t n = ringdim; \ - MODULE* module = new_module_info(ringdim, moduletype); \ - for (uint64_t sa : {dim1, dim2, dim3}) { \ - for (uint64_t sb : {dim1, dim2, dim3}) { \ - for (uint64_t sr : {dim1, dim2, dim3}) - -#define test_end() \ - } \ - } \ - free(module) - -void test_fft64_vec_znx_big_add(VEC_ZNX_BIG_ADD_F vec_znx_big_add_fcn) { - test_prelude(8, FFT64, 3, 5, 7) { - fft64_vec_znx_big_layout r(n, sr); - def_rand_big(a, n, sa); - def_rand_big(b, n, sb); - vec_znx_big_add_fcn(module, r.data, sr, a.data, sa, b.data, sb); - for (uint64_t i = 0; i < sr; ++i) { - ASSERT_EQ(r.get_copy(i), a.get_copy_zext(i) + b.get_copy_zext(i)); - } - } - test_end(); -} -void test_fft64_vec_znx_big_add_small(VEC_ZNX_BIG_ADD_SMALL_F vec_znx_big_add_fcn) { - test_prelude(16, FFT64, 2, 4, 5) { - fft64_vec_znx_big_layout r(n, sr); - def_rand_big(a, n, sa); - def_rand_small(b, n, sb); - vec_znx_big_add_fcn(module, r.data, sr, a.data, sa, b.data(), sb, 2 * n); - for (uint64_t i = 0; i < sr; ++i) { - ASSERT_EQ(r.get_copy(i), a.get_copy_zext(i) + b.get_copy_zext(i)); - } - } - test_end(); -} -void test_fft64_vec_znx_big_add_small2(VEC_ZNX_BIG_ADD_SMALL2_F vec_znx_big_add_fcn) { - test_prelude(64, FFT64, 3, 6, 7) { - fft64_vec_znx_big_layout r(n, sr); - def_rand_small(a, n, sa); - def_rand_small(b, n, sb); - vec_znx_big_add_fcn(module, r.data, sr, a.data(), sa, 2 * n, b.data(), sb, 2 * n); - for (uint64_t i = 0; i < sr; ++i) { - ASSERT_EQ(r.get_copy(i), a.get_copy_zext(i) + b.get_copy_zext(i)); - } - } - test_end(); -} - -TEST(fft64_vec_znx_big, fft64_vec_znx_big_add) { test_fft64_vec_znx_big_add(fft64_vec_znx_big_add); } -TEST(vec_znx_big, vec_znx_big_add) { test_fft64_vec_znx_big_add(vec_znx_big_add); } - -TEST(fft64_vec_znx_big, fft64_vec_znx_big_add_small) { test_fft64_vec_znx_big_add_small(fft64_vec_znx_big_add_small); } -TEST(vec_znx_big, vec_znx_big_add_small) { test_fft64_vec_znx_big_add_small(vec_znx_big_add_small); } - -TEST(fft64_vec_znx_big, fft64_vec_znx_big_add_small2) { - test_fft64_vec_znx_big_add_small2(fft64_vec_znx_big_add_small2); -} -TEST(vec_znx_big, vec_znx_big_add_small2) { test_fft64_vec_znx_big_add_small2(vec_znx_big_add_small2); } - -void test_fft64_vec_znx_big_sub(VEC_ZNX_BIG_SUB_F vec_znx_big_sub_fcn) { - test_prelude(16, FFT64, 3, 5, 7) { - fft64_vec_znx_big_layout r(n, sr); - def_rand_big(a, n, sa); - def_rand_big(b, n, sb); - vec_znx_big_sub_fcn(module, r.data, sr, a.data, sa, b.data, sb); - for (uint64_t i = 0; i < sr; ++i) { - ASSERT_EQ(r.get_copy(i), a.get_copy_zext(i) - b.get_copy_zext(i)); - } - } - test_end(); -} -void test_fft64_vec_znx_big_sub_small_a(VEC_ZNX_BIG_SUB_SMALL_A_F vec_znx_big_sub_fcn) { - test_prelude(32, FFT64, 2, 4, 5) { - fft64_vec_znx_big_layout r(n, sr); - def_rand_small(a, n, sa); - def_rand_big(b, n, sb); - vec_znx_big_sub_fcn(module, r.data, sr, a.data(), sa, 2 * n, b.data, sb); - for (uint64_t i = 0; i < sr; ++i) { - ASSERT_EQ(r.get_copy(i), a.get_copy_zext(i) - b.get_copy_zext(i)); - } - } - test_end(); -} -void test_fft64_vec_znx_big_sub_small_b(VEC_ZNX_BIG_SUB_SMALL_B_F vec_znx_big_sub_fcn) { - test_prelude(16, FFT64, 2, 4, 5) { - fft64_vec_znx_big_layout r(n, sr); - def_rand_big(a, n, sa); - def_rand_small(b, n, sb); - vec_znx_big_sub_fcn(module, r.data, sr, a.data, sa, b.data(), sb, 2 * n); - for (uint64_t i = 0; i < sr; ++i) { - ASSERT_EQ(r.get_copy(i), a.get_copy_zext(i) - b.get_copy_zext(i)); - } - } - test_end(); -} -void test_fft64_vec_znx_big_sub_small2(VEC_ZNX_BIG_SUB_SMALL2_F vec_znx_big_sub_fcn) { - test_prelude(8, FFT64, 3, 6, 7) { - fft64_vec_znx_big_layout r(n, sr); - def_rand_small(a, n, sa); - def_rand_small(b, n, sb); - vec_znx_big_sub_fcn(module, r.data, sr, a.data(), sa, 2 * n, b.data(), sb, 2 * n); - for (uint64_t i = 0; i < sr; ++i) { - ASSERT_EQ(r.get_copy(i), a.get_copy_zext(i) - b.get_copy_zext(i)); - } - } - test_end(); -} - -TEST(fft64_vec_znx_big, fft64_vec_znx_big_sub) { test_fft64_vec_znx_big_sub(fft64_vec_znx_big_sub); } -TEST(vec_znx_big, vec_znx_big_sub) { test_fft64_vec_znx_big_sub(vec_znx_big_sub); } - -TEST(fft64_vec_znx_big, fft64_vec_znx_big_sub_small_a) { - test_fft64_vec_znx_big_sub_small_a(fft64_vec_znx_big_sub_small_a); -} -TEST(vec_znx_big, vec_znx_big_sub_small_a) { test_fft64_vec_znx_big_sub_small_a(vec_znx_big_sub_small_a); } - -TEST(fft64_vec_znx_big, fft64_vec_znx_big_sub_small_b) { - test_fft64_vec_znx_big_sub_small_b(fft64_vec_znx_big_sub_small_b); -} -TEST(vec_znx_big, vec_znx_big_sub_small_b) { test_fft64_vec_znx_big_sub_small_b(vec_znx_big_sub_small_b); } - -TEST(fft64_vec_znx_big, fft64_vec_znx_big_sub_small2) { - test_fft64_vec_znx_big_sub_small2(fft64_vec_znx_big_sub_small2); -} -TEST(vec_znx_big, vec_znx_big_sub_small2) { test_fft64_vec_znx_big_sub_small2(vec_znx_big_sub_small2); } - -static void test_vec_znx_big_normalize(VEC_ZNX_BIG_NORMALIZE_BASE2K_F normalize, - VEC_ZNX_BIG_NORMALIZE_BASE2K_TMP_BYTES_F normalize_tmp_bytes) { - // in the FFT64 case, big_normalize is just a forward. - // we will just test that the functions are callable - uint64_t n = 16; - uint64_t k = 12; - MODULE* module = new_module_info(n, FFT64); - for (uint64_t sa : {3, 5, 7}) { - for (uint64_t sr : {3, 5, 7}) { - uint64_t r_sl = n + 3; - def_rand_big(a, n, sa); - znx_vec_i64_layout r(n, sr, r_sl); - std::vector<uint8_t> tmp_space(normalize_tmp_bytes(module)); - normalize(module, k, r.data(), sr, r_sl, a.data, sa, tmp_space.data()); - } - } - delete_module_info(module); -} - -TEST(vec_znx_big, fft64_vec_znx_big_normalize_base2k) { - test_vec_znx_big_normalize(fft64_vec_znx_big_normalize_base2k, fft64_vec_znx_big_normalize_base2k_tmp_bytes); -} -TEST(vec_znx_big, vec_znx_big_normalize_base2k) { - test_vec_znx_big_normalize(vec_znx_big_normalize_base2k, vec_znx_big_normalize_base2k_tmp_bytes); -} - -static void test_vec_znx_big_range_normalize( // - VEC_ZNX_BIG_RANGE_NORMALIZE_BASE2K_F normalize, - VEC_ZNX_BIG_RANGE_NORMALIZE_BASE2K_TMP_BYTES_F normalize_tmp_bytes) { - // in the FFT64 case, big_normalize is just a forward. - // we will just test that the functions are callable - uint64_t n = 16; - uint64_t k = 11; - MODULE* module = new_module_info(n, FFT64); - for (uint64_t sa : {6, 15, 21}) { - for (uint64_t sr : {3, 5, 7}) { - uint64_t r_sl = n + 3; - def_rand_big(a, n, sa); - uint64_t a_start = uniform_u64_bits(30) % (sa / 2); - uint64_t a_end = sa - (uniform_u64_bits(30) % (sa / 2)); - uint64_t a_step = (uniform_u64_bits(30) % 3) + 1; - uint64_t range_size = (a_end + a_step - 1 - a_start) / a_step; - fft64_vec_znx_big_layout aextr(n, range_size); - for (uint64_t i = 0, idx = a_start; idx < a_end; ++i, idx += a_step) { - aextr.set(i, a.get_copy(idx)); - } - znx_vec_i64_layout r(n, sr, r_sl); - znx_vec_i64_layout r2(n, sr, r_sl); - // tmp_space is large-enough for both - std::vector<uint8_t> tmp_space(normalize_tmp_bytes(module)); - normalize(module, k, r.data(), sr, r_sl, a.data, a_start, a_end, a_step, tmp_space.data()); - fft64_vec_znx_big_normalize_base2k(module, k, r2.data(), sr, r_sl, aextr.data, range_size, tmp_space.data()); - for (uint64_t i = 0; i < sr; ++i) { - ASSERT_EQ(r.get_copy(i), r2.get_copy(i)); - } - } - } - delete_module_info(module); -} - -TEST(vec_znx_big, fft64_vec_znx_big_range_normalize_base2k) { - test_vec_znx_big_range_normalize(fft64_vec_znx_big_range_normalize_base2k, - fft64_vec_znx_big_range_normalize_base2k_tmp_bytes); -} -TEST(vec_znx_big, vec_znx_big_range_normalize_base2k) { - test_vec_znx_big_range_normalize(vec_znx_big_range_normalize_base2k, vec_znx_big_range_normalize_base2k_tmp_bytes); -} - -static void test_vec_znx_big_rotate(VEC_ZNX_BIG_ROTATE_F rotate) { - // in the FFT64 case, big_normalize is just a forward. - // we will just test that the functions are callable - uint64_t n = 16; - int64_t p = 12; - MODULE* module = new_module_info(n, FFT64); - for (uint64_t sa : {3, 5, 7}) { - for (uint64_t sr : {3, 5, 7}) { - def_rand_big(a, n, sa); - fft64_vec_znx_big_layout r(n, sr); - rotate(module, p, r.data, sr, a.data, sa); - for (uint64_t i = 0; i < sr; ++i) { - znx_i64 aa = a.get_copy_zext(i); - znx_i64 expect(n); - for (uint64_t j = 0; j < n; ++j) { - expect.set_coeff(j, aa.get_coeff(int64_t(j) - p)); - } - znx_i64 actual = r.get_copy(i); - ASSERT_EQ(expect, actual); - } - } - } - delete_module_info(module); -} - -TEST(vec_znx_big, fft64_vec_znx_big_rotate) { test_vec_znx_big_rotate(fft64_vec_znx_big_rotate); } -TEST(vec_znx_big, vec_znx_big_rotate) { test_vec_znx_big_rotate(vec_znx_big_rotate); } - -static void test_vec_znx_big_automorphism(VEC_ZNX_BIG_AUTOMORPHISM_F automorphism) { - // in the FFT64 case, big_normalize is just a forward. - // we will just test that the functions are callable - uint64_t n = 16; - int64_t p = 11; - MODULE* module = new_module_info(n, FFT64); - for (uint64_t sa : {3, 5, 7}) { - for (uint64_t sr : {3, 5, 7}) { - def_rand_big(a, n, sa); - fft64_vec_znx_big_layout r(n, sr); - automorphism(module, p, r.data, sr, a.data, sa); - for (uint64_t i = 0; i < sr; ++i) { - znx_i64 aa = a.get_copy_zext(i); - znx_i64 expect(n); - for (uint64_t j = 0; j < n; ++j) { - expect.set_coeff(p * j, aa.get_coeff(j)); - } - znx_i64 actual = r.get_copy(i); - ASSERT_EQ(expect, actual); - } - } - } - delete_module_info(module); -} - -TEST(vec_znx_big, fft64_vec_znx_big_automorphism) { test_vec_znx_big_automorphism(fft64_vec_znx_big_automorphism); } -TEST(vec_znx_big, vec_znx_big_automorphism) { test_vec_znx_big_automorphism(vec_znx_big_automorphism); }
D spqlios/lib/test/spqlios_vec_znx_dft_test.cpp

@@ -1,193 +0,0 @@

-#include <gtest/gtest.h> - -#include <cstdint> - -#include "../spqlios/arithmetic/vec_znx_arithmetic_private.h" -#include "spqlios/arithmetic/vec_znx_arithmetic.h" -#include "test/testlib/ntt120_dft.h" -#include "test/testlib/ntt120_layouts.h" -#include "testlib/fft64_dft.h" -#include "testlib/fft64_layouts.h" -#include "testlib/polynomial_vector.h" - -static void test_fft64_vec_znx_dft(VEC_ZNX_DFT_F dft) { - for (uint64_t n : {2, 4, 128}) { - MODULE* module = new_module_info(n, FFT64); - for (uint64_t sa : {3, 5, 8}) { - for (uint64_t sr : {3, 5, 8}) { - uint64_t a_sl = n + uniform_u64_bits(2); - znx_vec_i64_layout a(n, sa, a_sl); - fft64_vec_znx_dft_layout res(n, sr); - a.fill_random(42); - std::vector<reim_fft64vec> expect(sr); - for (uint64_t i = 0; i < sr; ++i) { - expect[i] = simple_fft64(a.get_copy_zext(i)); - } - // test the function - thash hash_before = a.content_hash(); - dft(module, res.data, sr, a.data(), sa, a_sl); - ASSERT_EQ(a.content_hash(), hash_before); - for (uint64_t i = 0; i < sr; ++i) { - reim_fft64vec actual = res.get_copy_zext(i); - ASSERT_LE(infty_dist(actual, expect[i]), 1e-10); - } - } - } - delete_module_info(module); - } -} - -#ifdef __x86_64__ -// FIXME: currently, it only works on avx -static void test_ntt120_vec_znx_dft(VEC_ZNX_DFT_F dft) { - for (uint64_t n : {2, 4, 128}) { - MODULE* module = new_module_info(n, NTT120); - for (uint64_t sa : {3, 5, 8}) { - for (uint64_t sr : {3, 5, 8}) { - uint64_t a_sl = n + uniform_u64_bits(2); - znx_vec_i64_layout a(n, sa, a_sl); - ntt120_vec_znx_dft_layout res(n, sr); - a.fill_random(42); - std::vector<q120_nttvec> expect(sr); - for (uint64_t i = 0; i < sr; ++i) { - expect[i] = simple_ntt120(a.get_copy_zext(i)); - } - // test the function - thash hash_before = a.content_hash(); - dft(module, res.data, sr, a.data(), sa, a_sl); - ASSERT_EQ(a.content_hash(), hash_before); - for (uint64_t i = 0; i < sr; ++i) { - q120_nttvec actual = res.get_copy_zext(i); - if (!(actual == expect[i])) { - for (uint64_t j = 0; j < n; ++j) { - std::cerr << actual.v[j] << " vs " << expect[i].v[j] << std::endl; - } - } - ASSERT_EQ(actual, expect[i]); - } - } - } - delete_module_info(module); - } -} -#endif - -TEST(vec_znx_dft, fft64_vec_znx_dft) { test_fft64_vec_znx_dft(fft64_vec_znx_dft); } -#ifdef __x86_64__ -// FIXME: currently, it only works on avx -TEST(vec_znx_dft, ntt120_vec_znx_dft) { test_ntt120_vec_znx_dft(ntt120_vec_znx_dft_avx); } -#endif -TEST(vec_znx_dft, vec_znx_dft) { - test_fft64_vec_znx_dft(vec_znx_dft); -#ifdef __x86_64__ - // FIXME: currently, it only works on avx - test_ntt120_vec_znx_dft(ntt120_vec_znx_dft_avx); -#endif -} - -static void test_fft64_vec_znx_idft(VEC_ZNX_IDFT_F idft, VEC_ZNX_IDFT_TMP_A_F idft_tmp_a, - VEC_ZNX_IDFT_TMP_BYTES_F idft_tmp_bytes) { - for (uint64_t n : {2, 4, 64, 128}) { - MODULE* module = new_module_info(n, FFT64); - uint64_t tmp_size = idft_tmp_bytes ? idft_tmp_bytes(module) : 0; - std::vector<uint8_t> tmp(tmp_size); - for (uint64_t sa : {3, 5, 8}) { - for (uint64_t sr : {3, 5, 8}) { - fft64_vec_znx_dft_layout a(n, sa); - fft64_vec_znx_big_layout res(n, sr); - a.fill_dft_random_log2bound(22); - std::vector<znx_i64> expect(sr); - for (uint64_t i = 0; i < sr; ++i) { - expect[i] = simple_rint_ifft64(a.get_copy_zext(i)); - } - // test the function - if (idft_tmp_bytes) { - thash hash_before = a.content_hash(); - idft(module, res.data, sr, a.data, sa, tmp.data()); - ASSERT_EQ(a.content_hash(), hash_before); - } else { - idft_tmp_a(module, res.data, sr, a.data, sa); - } - for (uint64_t i = 0; i < sr; ++i) { - znx_i64 actual = res.get_copy_zext(i); - // ASSERT_EQ(res.get_copy_zext(i), expect[i]); - if (!(actual == expect[i])) { - for (uint64_t j = 0; j < n; ++j) { - std::cerr << actual.get_coeff(j) << " dft vs. " << expect[i].get_coeff(j) << std::endl; - } - FAIL(); - } - } - } - } - delete_module_info(module); - } -} - -TEST(vec_znx_dft, fft64_vec_znx_idft) { - test_fft64_vec_znx_idft(fft64_vec_znx_idft, nullptr, fft64_vec_znx_idft_tmp_bytes); -} -TEST(vec_znx_dft, fft64_vec_znx_idft_tmp_a) { test_fft64_vec_znx_idft(nullptr, fft64_vec_znx_idft_tmp_a, nullptr); } - -#ifdef __x86_64__ -// FIXME: currently, it only works on avx -static void test_ntt120_vec_znx_idft(VEC_ZNX_IDFT_F idft, VEC_ZNX_IDFT_TMP_A_F idft_tmp_a, - VEC_ZNX_IDFT_TMP_BYTES_F idft_tmp_bytes) { - for (uint64_t n : {2, 4, 64, 128}) { - MODULE* module = new_module_info(n, NTT120); - uint64_t tmp_size = idft_tmp_bytes ? idft_tmp_bytes(module) : 0; - std::vector<uint8_t> tmp(tmp_size); - for (uint64_t sa : {3, 5, 8}) { - for (uint64_t sr : {3, 5, 8}) { - ntt120_vec_znx_dft_layout a(n, sa); - ntt120_vec_znx_big_layout res(n, sr); - a.fill_random(); - std::vector<znx_i128> expect(sr); - for (uint64_t i = 0; i < sr; ++i) { - expect[i] = simple_intt120(a.get_copy_zext(i)); - } - // test the function - if (idft_tmp_bytes) { - thash hash_before = a.content_hash(); - idft(module, res.data, sr, a.data, sa, tmp.data()); - ASSERT_EQ(a.content_hash(), hash_before); - } else { - idft_tmp_a(module, res.data, sr, a.data, sa); - } - for (uint64_t i = 0; i < sr; ++i) { - znx_i128 actual = res.get_copy_zext(i); - ASSERT_EQ(res.get_copy_zext(i), expect[i]); - // if (!(actual == expect[i])) { - // for (uint64_t j = 0; j < n; ++j) { - // std::cerr << actual.get_coeff(j) << " dft vs. " << expect[i].get_coeff(j) << std::endl; - // } - // FAIL(); - // } - } - } - } - delete_module_info(module); - } -} - -TEST(vec_znx_dft, ntt120_vec_znx_idft) { - test_ntt120_vec_znx_idft(ntt120_vec_znx_idft_avx, nullptr, ntt120_vec_znx_idft_tmp_bytes_avx); -} -TEST(vec_znx_dft, ntt120_vec_znx_idft_tmp_a) { - test_ntt120_vec_znx_idft(nullptr, ntt120_vec_znx_idft_tmp_a_avx, nullptr); -} -#endif -TEST(vec_znx_dft, vec_znx_idft) { - test_fft64_vec_znx_idft(vec_znx_idft, nullptr, vec_znx_idft_tmp_bytes); -#ifdef __x86_64__ - // FIXME: currently, only supported on avx - test_ntt120_vec_znx_idft(vec_znx_idft, nullptr, vec_znx_idft_tmp_bytes); -#endif -} -TEST(vec_znx_dft, vec_znx_idft_tmp_a) { - test_fft64_vec_znx_idft(nullptr, vec_znx_idft_tmp_a, nullptr); -#ifdef __x86_64__ - // FIXME: currently, only supported on avx - test_ntt120_vec_znx_idft(nullptr, vec_znx_idft_tmp_a, nullptr); -#endif -}
D spqlios/lib/test/spqlios_vec_znx_test.cpp

@@ -1,546 +0,0 @@

-#include <cstdint> -#include <utility> - -#include "../spqlios/arithmetic/vec_znx_arithmetic.h" -#include "gtest/gtest.h" -#include "spqlios/arithmetic/vec_znx_arithmetic_private.h" -#include "spqlios/coeffs/coeffs_arithmetic.h" -#include "test/testlib/mod_q120.h" -#include "test/testlib/negacyclic_polynomial.h" -#include "testlib/fft64_dft.h" -#include "testlib/polynomial_vector.h" - -TEST(fft64_layouts, dft_idft_fft64) { - uint64_t n = 128; - // create a random polynomial - znx_i64 p(n); - for (uint64_t i = 0; i < n; ++i) { - p.set_coeff(i, uniform_i64_bits(36)); - } - // call fft - reim_fft64vec q = simple_fft64(p); - // call ifft and round - znx_i64 r = simple_rint_ifft64(q); - ASSERT_EQ(p, r); -} - -TEST(znx_layout, valid_test) { - uint64_t n = 4; - znx_vec_i64_layout v(n, 7, 13); - // this should be ok - v.set(0, znx_i64::zero(n)); - // this should be ok - ASSERT_EQ(v.get_copy_zext(0), znx_i64::zero(n)); - ASSERT_EQ(v.data()[2], 0); // should be ok - // this is also ok (zero extended vector) - ASSERT_EQ(v.get_copy_zext(1000), znx_i64::zero(n)); -} - -// disabling this test by default, since it depicts on purpose wrong accesses -#if 0 -TEST(znx_layout, valgrind_antipattern_test) { - uint64_t n = 4; - znx_vec_i64_layout v(n, 7, 13); - // this should be ok - v.set(0, znx_i64::zero(n)); - // this should abort (wrong ring dimension) - ASSERT_DEATH(v.set(3, znx_i64::zero(2 * n)), ""); - // this should abort (out of bounds) - ASSERT_DEATH(v.set(8, znx_i64::zero(n)), ""); - // this should be ok - ASSERT_EQ(v.get_copy_zext(0), znx_i64::zero(n)); - // should be an uninit read - ASSERT_TRUE(!(v.get_copy_zext(2) == znx_i64::zero(n))); // should be uninit - // should be an invalid read (inter-slice) - ASSERT_NE(v.data()[4], 0); - ASSERT_EQ(v.data()[2], 0); // should be ok - // should be an uninit read - ASSERT_NE(v.data()[13], 0); // should be uninit -} -#endif - -// test of binary operations - -// test for out of place calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_binop_outplace(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - for (uint64_t n : {2, 4, 8, 128}) { - MODULE_TYPE mtype = uniform_u64() % 2 == 0 ? FFT64 : NTT120; - MODULE* mod = new_module_info(n, mtype); - for (uint64_t sa : {7, 13, 15}) { - for (uint64_t sb : {7, 13, 15}) { - for (uint64_t sc : {7, 13, 15}) { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - uint64_t b_sl = uniform_u64_bits(3) * 5 + n; - uint64_t c_sl = uniform_u64_bits(3) * 5 + n; - znx_vec_i64_layout la(n, sa, a_sl); - znx_vec_i64_layout lb(n, sb, b_sl); - znx_vec_i64_layout lc(n, sc, c_sl); - std::vector<znx_i64> expect(sc); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, znx_i64::random_log2bound(n, 62)); - } - for (uint64_t i = 0; i < sb; ++i) { - lb.set(i, znx_i64::random_log2bound(n, 62)); - } - for (uint64_t i = 0; i < sc; ++i) { - expect[i] = ref_binop(la.get_copy_zext(i), lb.get_copy_zext(i)); - } - binop(mod, // N - lc.data(), sc, c_sl, // res - la.data(), sa, a_sl, // a - lb.data(), sb, b_sl); - for (uint64_t i = 0; i < sc; ++i) { - ASSERT_EQ(lc.get_copy_zext(i), expect[i]); - } - } - } - } - delete_module_info(mod); - } -} -// test for inplace1 calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_binop_inplace1(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - for (uint64_t n : {2, 4, 64}) { - MODULE_TYPE mtype = uniform_u64() % 2 == 0 ? FFT64 : NTT120; - MODULE* mod = new_module_info(n, mtype); - for (uint64_t sa : {3, 9, 12}) { - for (uint64_t sb : {3, 9, 12}) { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - uint64_t b_sl = uniform_u64_bits(3) * 5 + n; - znx_vec_i64_layout la(n, sa, a_sl); - znx_vec_i64_layout lb(n, sb, b_sl); - std::vector<znx_i64> expect(sa); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, znx_i64::random_log2bound(n, 62)); - } - for (uint64_t i = 0; i < sb; ++i) { - lb.set(i, znx_i64::random_log2bound(n, 62)); - } - for (uint64_t i = 0; i < sa; ++i) { - expect[i] = ref_binop(la.get_copy_zext(i), lb.get_copy_zext(i)); - } - binop(mod, // N - la.data(), sa, a_sl, // res - la.data(), sa, a_sl, // a - lb.data(), sb, b_sl); - for (uint64_t i = 0; i < sa; ++i) { - ASSERT_EQ(la.get_copy_zext(i), expect[i]); - } - } - } - delete_module_info(mod); - } -} -// test for inplace2 calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_binop_inplace2(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - for (uint64_t n : {4, 32, 64}) { - MODULE_TYPE mtype = uniform_u64() % 2 == 0 ? FFT64 : NTT120; - MODULE* mod = new_module_info(n, mtype); - for (uint64_t sa : {3, 9, 12}) { - for (uint64_t sb : {3, 9, 12}) { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - uint64_t b_sl = uniform_u64_bits(3) * 5 + n; - znx_vec_i64_layout la(n, sa, a_sl); - znx_vec_i64_layout lb(n, sb, b_sl); - std::vector<znx_i64> expect(sb); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, znx_i64::random_log2bound(n, 62)); - } - for (uint64_t i = 0; i < sb; ++i) { - lb.set(i, znx_i64::random_log2bound(n, 62)); - } - for (uint64_t i = 0; i < sb; ++i) { - expect[i] = ref_binop(la.get_copy_zext(i), lb.get_copy_zext(i)); - } - binop(mod, // N - lb.data(), sb, b_sl, // res - la.data(), sa, a_sl, // a - lb.data(), sb, b_sl); - for (uint64_t i = 0; i < sb; ++i) { - ASSERT_EQ(lb.get_copy_zext(i), expect[i]); - } - } - } - delete_module_info(mod); - } -} -// test for inplace3 calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_binop_inplace3(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - for (uint64_t n : {2, 16, 1024}) { - MODULE_TYPE mtype = uniform_u64() % 2 == 0 ? FFT64 : NTT120; - MODULE* mod = new_module_info(n, mtype); - for (uint64_t sa : {2, 6, 11}) { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - znx_vec_i64_layout la(n, sa, a_sl); - std::vector<znx_i64> expect(sa); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, znx_i64::random_log2bound(n, 62)); - } - for (uint64_t i = 0; i < sa; ++i) { - expect[i] = ref_binop(la.get_copy_zext(i), la.get_copy_zext(i)); - } - binop(mod, // N - la.data(), sa, a_sl, // res - la.data(), sa, a_sl, // a - la.data(), sa, a_sl); - for (uint64_t i = 0; i < sa; ++i) { - ASSERT_EQ(la.get_copy_zext(i), expect[i]); - } - } - delete_module_info(mod); - } -} -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_binop(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - test_vec_znx_elemw_binop_outplace(binop, ref_binop); - test_vec_znx_elemw_binop_inplace1(binop, ref_binop); - test_vec_znx_elemw_binop_inplace2(binop, ref_binop); - test_vec_znx_elemw_binop_inplace3(binop, ref_binop); -} - -static znx_i64 poly_add(const znx_i64& a, const znx_i64& b) { return a + b; } -TEST(vec_znx, vec_znx_add) { test_vec_znx_elemw_binop(vec_znx_add, poly_add); } -TEST(vec_znx, vec_znx_add_ref) { test_vec_znx_elemw_binop(vec_znx_add_ref, poly_add); } -#ifdef __x86_64__ -TEST(vec_znx, vec_znx_add_avx) { test_vec_znx_elemw_binop(vec_znx_add_avx, poly_add); } -#endif - -static znx_i64 poly_sub(const znx_i64& a, const znx_i64& b) { return a - b; } -TEST(vec_znx, vec_znx_sub) { test_vec_znx_elemw_binop(vec_znx_sub, poly_sub); } -TEST(vec_znx, vec_znx_sub_ref) { test_vec_znx_elemw_binop(vec_znx_sub_ref, poly_sub); } -#ifdef __x86_64__ -TEST(vec_znx, vec_znx_sub_avx) { test_vec_znx_elemw_binop(vec_znx_sub_avx, poly_sub); } -#endif - -// test of rotation operations - -// test for out of place calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_unop_param_outplace(ACTUAL_FCN test_rotate, EXPECT_FCN ref_rotate, int64_t (*param_gen)()) { - for (uint64_t n : {2, 4, 8, 128}) { - MODULE_TYPE mtype = uniform_u64() % 2 == 0 ? FFT64 : NTT120; - MODULE* mod = new_module_info(n, mtype); - for (uint64_t sa : {7, 13, 15}) { - for (uint64_t sb : {7, 13, 15}) { - { - int64_t p = param_gen(); - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - uint64_t b_sl = uniform_u64_bits(3) * 4 + n; - znx_vec_i64_layout la(n, sa, a_sl); - znx_vec_i64_layout lb(n, sb, b_sl); - std::vector<znx_i64> expect(sb); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, znx_i64::random_log2bound(n, 62)); - } - for (uint64_t i = 0; i < sb; ++i) { - expect[i] = ref_rotate(p, la.get_copy_zext(i)); - } - test_rotate(mod, // - p, // - lb.data(), sb, b_sl, // - la.data(), sa, a_sl // - ); - for (uint64_t i = 0; i < sb; ++i) { - ASSERT_EQ(lb.get_copy_zext(i), expect[i]) << n << " " << sa << " " << sb << " " << i; - } - } - } - } - delete_module_info(mod); - } -} - -// test for inplace calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_unop_param_inplace(ACTUAL_FCN test_rotate, EXPECT_FCN ref_rotate, int64_t (*param_gen)()) { - for (uint64_t n : {2, 16, 1024}) { - MODULE_TYPE mtype = uniform_u64() % 2 == 0 ? FFT64 : NTT120; - MODULE* mod = new_module_info(n, mtype); - for (uint64_t sa : {2, 6, 11}) { - { - int64_t p = param_gen(); - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - znx_vec_i64_layout la(n, sa, a_sl); - std::vector<znx_i64> expect(sa); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, znx_i64::random_log2bound(n, 62)); - } - for (uint64_t i = 0; i < sa; ++i) { - expect[i] = ref_rotate(p, la.get_copy_zext(i)); - } - test_rotate(mod, // N - p, //; - la.data(), sa, a_sl, // res - la.data(), sa, a_sl // a - ); - for (uint64_t i = 0; i < sa; ++i) { - ASSERT_EQ(la.get_copy_zext(i), expect[i]) << n << " " << sa << " " << i; - } - } - } - delete_module_info(mod); - } -} - -static int64_t random_rotate_param() { return uniform_i64(); } - -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_rotate(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - test_vec_znx_elemw_unop_param_outplace(binop, ref_binop, random_rotate_param); - test_vec_znx_elemw_unop_param_inplace(binop, ref_binop, random_rotate_param); -} - -static znx_i64 poly_rotate(const int64_t p, const znx_i64& a) { - uint64_t n = a.nn(); - znx_i64 res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, a.get_coeff(i - p)); - } - return res; -} -TEST(vec_znx, vec_znx_rotate) { test_vec_znx_elemw_rotate(vec_znx_rotate, poly_rotate); } -TEST(vec_znx, vec_znx_rotate_ref) { test_vec_znx_elemw_rotate(vec_znx_rotate_ref, poly_rotate); } - -static int64_t random_automorphism_param() { return uniform_i64() | 1; } - -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_automorphism(ACTUAL_FCN unop, EXPECT_FCN ref_unop) { - test_vec_znx_elemw_unop_param_outplace(unop, ref_unop, random_automorphism_param); - test_vec_znx_elemw_unop_param_inplace(unop, ref_unop, random_automorphism_param); -} - -static znx_i64 poly_automorphism(const int64_t p, const znx_i64& a) { - uint64_t n = a.nn(); - znx_i64 res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i * p, a.get_coeff(i)); - } - return res; -} - -TEST(vec_znx, vec_znx_automorphism) { test_vec_znx_elemw_automorphism(vec_znx_automorphism, poly_automorphism); } -TEST(vec_znx, vec_znx_automorphism_ref) { - test_vec_znx_elemw_automorphism(vec_znx_automorphism_ref, poly_automorphism); -} - -// test for out of place calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_unop_outplace(ACTUAL_FCN test_unop, EXPECT_FCN ref_unop) { - for (uint64_t n : {2, 4, 8, 128}) { - MODULE_TYPE mtype = uniform_u64() % 2 == 0 ? FFT64 : NTT120; - MODULE* mod = new_module_info(n, mtype); - for (uint64_t sa : {7, 13, 15}) { - for (uint64_t sb : {7, 13, 15}) { - { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - uint64_t b_sl = uniform_u64_bits(3) * 4 + n; - znx_vec_i64_layout la(n, sa, a_sl); - znx_vec_i64_layout lb(n, sb, b_sl); - std::vector<znx_i64> expect(sb); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, znx_i64::random_log2bound(n, 62)); - } - for (uint64_t i = 0; i < sb; ++i) { - expect[i] = ref_unop(la.get_copy_zext(i)); - } - test_unop(mod, // - lb.data(), sb, b_sl, // - la.data(), sa, a_sl // - ); - for (uint64_t i = 0; i < sb; ++i) { - ASSERT_EQ(lb.get_copy_zext(i), expect[i]) << n << " " << sa << " " << sb << " " << i; - } - } - } - } - delete_module_info(mod); - } -} - -// test for inplace calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_unop_inplace(ACTUAL_FCN test_unop, EXPECT_FCN ref_unop) { - for (uint64_t n : {2, 16, 1024}) { - MODULE_TYPE mtype = uniform_u64() % 2 == 0 ? FFT64 : NTT120; - MODULE* mod = new_module_info(n, mtype); - for (uint64_t sa : {2, 6, 11}) { - { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - znx_vec_i64_layout la(n, sa, a_sl); - std::vector<znx_i64> expect(sa); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, znx_i64::random_log2bound(n, 62)); - } - for (uint64_t i = 0; i < sa; ++i) { - expect[i] = ref_unop(la.get_copy_zext(i)); - } - test_unop(mod, // N - la.data(), sa, a_sl, // res - la.data(), sa, a_sl // a - ); - for (uint64_t i = 0; i < sa; ++i) { - ASSERT_EQ(la.get_copy_zext(i), expect[i]) << n << " " << sa << " " << i; - } - } - } - delete_module_info(mod); - } -} - -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_unop(ACTUAL_FCN unop, EXPECT_FCN ref_unop) { - test_vec_znx_elemw_unop_outplace(unop, ref_unop); - test_vec_znx_elemw_unop_inplace(unop, ref_unop); -} - -static znx_i64 poly_copy(const znx_i64& a) { return a; } - -TEST(vec_znx, vec_znx_copy) { test_vec_znx_elemw_unop(vec_znx_copy, poly_copy); } -TEST(vec_znx, vec_znx_copy_ref) { test_vec_znx_elemw_unop(vec_znx_copy_ref, poly_copy); } - -static znx_i64 poly_negate(const znx_i64& a) { return -a; } - -TEST(vec_znx, vec_znx_negate) { test_vec_znx_elemw_unop(vec_znx_negate, poly_negate); } -TEST(vec_znx, vec_znx_negate_ref) { test_vec_znx_elemw_unop(vec_znx_negate_ref, poly_negate); } -#ifdef __x86_64__ -TEST(vec_znx, vec_znx_negate_avx) { test_vec_znx_elemw_unop(vec_znx_negate_avx, poly_negate); } -#endif - -static void test_vec_znx_zero(VEC_ZNX_ZERO_F zero) { - for (uint64_t n : {2, 16, 1024}) { - MODULE_TYPE mtype = uniform_u64() % 2 == 0 ? FFT64 : NTT120; - MODULE* mod = new_module_info(n, mtype); - for (uint64_t sa : {2, 6, 11}) { - { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - znx_vec_i64_layout la(n, sa, a_sl); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, znx_i64::random_log2bound(n, 62)); - } - zero(mod, // N - la.data(), sa, a_sl // res - ); - znx_i64 ZERO = znx_i64::zero(n); - for (uint64_t i = 0; i < sa; ++i) { - ASSERT_EQ(la.get_copy_zext(i), ZERO) << n << " " << sa << " " << i; - } - } - } - delete_module_info(mod); - } -} - -TEST(vec_znx, vec_znx_zero) { test_vec_znx_zero(vec_znx_zero); } -TEST(vec_znx, vec_znx_zero_ref) { test_vec_znx_zero(vec_znx_zero_ref); } - -static void vec_poly_normalize(const uint64_t base_k, std::vector<znx_i64>& in) { - if (in.size() > 0) { - uint64_t n = in.front().nn(); - - znx_i64 out = znx_i64::random_log2bound(n, 62); - znx_i64 cinout(n); - for (int64_t i = in.size() - 1; i >= 0; --i) { - znx_normalize(n, base_k, in[i].data(), cinout.data(), in[i].data(), cinout.data()); - } - } -} - -template <typename ACTUAL_FCN, typename TMP_BYTES_FNC> -void test_vec_znx_normalize_outplace(ACTUAL_FCN test_normalize, TMP_BYTES_FNC tmp_bytes) { - for (uint64_t n : {2, 16, 1024}) { - MODULE_TYPE mtype = uniform_u64() % 2 == 0 ? FFT64 : NTT120; - MODULE* mod = new_module_info(n, mtype); - for (uint64_t sa : {1, 2, 6, 11}) { - for (uint64_t sb : {1, 2, 6, 11}) { - for (uint64_t base_k : {19}) { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - znx_vec_i64_layout la(n, sa, a_sl); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, znx_i64::random_log2bound(n, 62)); - } - - std::vector<znx_i64> la_norm; - for (uint64_t i = 0; i < sa; ++i) { - la_norm.push_back(la.get_copy_zext(i)); - } - vec_poly_normalize(base_k, la_norm); - - uint64_t b_sl = uniform_u64_bits(3) * 5 + n; - znx_vec_i64_layout lb(n, sb, b_sl); - - const uint64_t tmp_size = tmp_bytes(mod); - uint8_t* tmp = new uint8_t[tmp_size]; - test_normalize(mod, // N - base_k, // base_k - lb.data(), sb, b_sl, // res - la.data(), sa, a_sl, // a - tmp); - delete[] tmp; - - for (uint64_t i = 0; i < std::min(sa, sb); ++i) { - ASSERT_EQ(lb.get_copy_zext(i), la_norm[i]) << n << " " << sa << " " << sb << " " << i; - } - znx_i64 zero(n); - for (uint64_t i = std::min(sa, sb); i < sb; ++i) { - ASSERT_EQ(lb.get_copy_zext(i), zero) << n << " " << sa << " " << sb << " " << i; - } - } - } - } - delete_module_info(mod); - } -} - -TEST(vec_znx, vec_znx_normalize_outplace) { - test_vec_znx_normalize_outplace(vec_znx_normalize_base2k, vec_znx_normalize_base2k_tmp_bytes); -} -TEST(vec_znx, vec_znx_normalize_outplace_ref) { - test_vec_znx_normalize_outplace(vec_znx_normalize_base2k_ref, vec_znx_normalize_base2k_tmp_bytes_ref); -} - -template <typename ACTUAL_FCN, typename TMP_BYTES_FNC> -void test_vec_znx_normalize_inplace(ACTUAL_FCN test_normalize, TMP_BYTES_FNC tmp_bytes) { - for (uint64_t n : {2, 16, 1024}) { - MODULE_TYPE mtype = uniform_u64() % 2 == 0 ? FFT64 : NTT120; - MODULE* mod = new_module_info(n, mtype); - for (uint64_t sa : {2, 6, 11}) { - for (uint64_t base_k : {19}) { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - znx_vec_i64_layout la(n, sa, a_sl); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, znx_i64::random_log2bound(n, 62)); - } - - std::vector<znx_i64> la_norm; - for (uint64_t i = 0; i < sa; ++i) { - la_norm.push_back(la.get_copy_zext(i)); - } - vec_poly_normalize(base_k, la_norm); - - const uint64_t tmp_size = tmp_bytes(mod); - uint8_t* tmp = new uint8_t[tmp_size]; - test_normalize(mod, // N - base_k, // base_k - la.data(), sa, a_sl, // res - la.data(), sa, a_sl, // a - tmp); - delete[] tmp; - for (uint64_t i = 0; i < sa; ++i) { - ASSERT_EQ(la.get_copy_zext(i), la_norm[i]) << n << " " << sa << " " << i; - } - } - } - delete_module_info(mod); - } -} - -TEST(vec_znx, vec_znx_normalize_inplace) { - test_vec_znx_normalize_inplace(vec_znx_normalize_base2k, vec_znx_normalize_base2k_tmp_bytes); -} -TEST(vec_znx, vec_znx_normalize_inplace_ref) { - test_vec_znx_normalize_inplace(vec_znx_normalize_base2k_ref, vec_znx_normalize_base2k_tmp_bytes_ref); -}
D spqlios/lib/test/spqlios_vmp_product_test.cpp

@@ -1,121 +0,0 @@

-#include <gtest/gtest.h> - -#include "../spqlios/arithmetic/vec_znx_arithmetic_private.h" -#include "testlib/fft64_layouts.h" -#include "testlib/polynomial_vector.h" - -static void test_vmp_prepare_contiguous(VMP_PREPARE_CONTIGUOUS_F* prepare_contiguous, - VMP_PREPARE_CONTIGUOUS_TMP_BYTES_F* tmp_bytes) { - // tests when n < 8 - for (uint64_t nn : {2, 4}) { - MODULE* module = new_module_info(nn, FFT64); - for (uint64_t nrows : {1, 2, 5}) { - for (uint64_t ncols : {2, 6, 7}) { - znx_vec_i64_layout mat(nn, nrows * ncols, nn); - fft64_vmp_pmat_layout pmat(nn, nrows, ncols); - mat.fill_random(30); - std::vector<uint8_t> tmp_space(fft64_vmp_prepare_contiguous_tmp_bytes(module, nrows, ncols)); - thash hash_before = mat.content_hash(); - prepare_contiguous(module, pmat.data, mat.data(), nrows, ncols, tmp_space.data()); - ASSERT_EQ(mat.content_hash(), hash_before); - for (uint64_t row = 0; row < nrows; ++row) { - for (uint64_t col = 0; col < ncols; ++col) { - const double* pmatv = (double*)pmat.data + (col * nrows + row) * nn; - reim_fft64vec tmp = simple_fft64(mat.get_copy(row * ncols + col)); - const double* tmpv = tmp.data(); - for (uint64_t i = 0; i < nn; ++i) { - ASSERT_LE(abs(pmatv[i] - tmpv[i]), 1e-10); - } - } - } - } - } - delete_module_info(module); - } - // tests when n >= 8 - for (uint64_t nn : {8, 32}) { - MODULE* module = new_module_info(nn, FFT64); - uint64_t nblk = nn / 8; - for (uint64_t nrows : {1, 2, 5}) { - for (uint64_t ncols : {2, 6, 7}) { - znx_vec_i64_layout mat(nn, nrows * ncols, nn); - fft64_vmp_pmat_layout pmat(nn, nrows, ncols); - mat.fill_random(30); - std::vector<uint8_t> tmp_space(tmp_bytes(module, nrows, ncols)); - thash hash_before = mat.content_hash(); - prepare_contiguous(module, pmat.data, mat.data(), nrows, ncols, tmp_space.data()); - ASSERT_EQ(mat.content_hash(), hash_before); - for (uint64_t row = 0; row < nrows; ++row) { - for (uint64_t col = 0; col < ncols; ++col) { - reim_fft64vec tmp = simple_fft64(mat.get_copy(row * ncols + col)); - for (uint64_t blk = 0; blk < nblk; ++blk) { - reim4_elem expect = tmp.get_blk(blk); - reim4_elem actual = pmat.get(row, col, blk); - ASSERT_LE(infty_dist(actual, expect), 1e-10); - } - } - } - } - } - delete_module_info(module); - } -} - -TEST(vec_znx, vmp_prepare_contiguous) { - test_vmp_prepare_contiguous(vmp_prepare_contiguous, vmp_prepare_contiguous_tmp_bytes); -} -TEST(vec_znx, fft64_vmp_prepare_contiguous_ref) { - test_vmp_prepare_contiguous(fft64_vmp_prepare_contiguous_ref, fft64_vmp_prepare_contiguous_tmp_bytes); -} -#ifdef __x86_64__ -TEST(vec_znx, fft64_vmp_prepare_contiguous_avx) { - test_vmp_prepare_contiguous(fft64_vmp_prepare_contiguous_avx, fft64_vmp_prepare_contiguous_tmp_bytes); -} -#endif - -static void test_vmp_apply(VMP_APPLY_DFT_TO_DFT_F* apply, VMP_APPLY_DFT_TO_DFT_TMP_BYTES_F* tmp_bytes) { - for (uint64_t nn : {2, 4, 8, 64}) { - MODULE* module = new_module_info(nn, FFT64); - for (uint64_t mat_nrows : {1, 4, 7}) { - for (uint64_t mat_ncols : {1, 2, 5}) { - for (uint64_t in_size : {1, 4, 7}) { - for (uint64_t out_size : {1, 2, 5}) { - fft64_vec_znx_dft_layout in(nn, in_size); - fft64_vmp_pmat_layout pmat(nn, mat_nrows, mat_ncols); - fft64_vec_znx_dft_layout out(nn, out_size); - in.fill_random(0); - pmat.fill_random(0); - // naive computation of the product - std::vector<reim_fft64vec> expect(out_size, reim_fft64vec(nn)); - for (uint64_t col = 0; col < out_size; ++col) { - reim_fft64vec ex = reim_fft64vec::zero(nn); - for (uint64_t row = 0; row < std::min(mat_nrows, in_size); ++row) { - ex += pmat.get_zext(row, col) * in.get_copy_zext(row); - } - expect[col] = ex; - } - // apply the product - std::vector<uint8_t> tmp(tmp_bytes(module, out_size, in_size, mat_nrows, mat_ncols)); - apply(module, out.data, out_size, in.data, in_size, pmat.data, mat_nrows, mat_ncols, tmp.data()); - // check that the output is close from the expectation - for (uint64_t col = 0; col < out_size; ++col) { - reim_fft64vec actual = out.get_copy_zext(col); - ASSERT_LE(infty_dist(actual, expect[col]), 1e-10); - } - } - } - } - } - delete_module_info(module); - } -} - -TEST(vec_znx, vmp_apply_to_dft) { test_vmp_apply(vmp_apply_dft_to_dft, vmp_apply_dft_to_dft_tmp_bytes); } -TEST(vec_znx, fft64_vmp_apply_dft_to_dft_ref) { - test_vmp_apply(fft64_vmp_apply_dft_to_dft_ref, fft64_vmp_apply_dft_to_dft_tmp_bytes); -} -#ifdef __x86_64__ -TEST(vec_znx, fft64_vmp_apply_dft_to_dft_avx) { - test_vmp_apply(fft64_vmp_apply_dft_to_dft_avx, fft64_vmp_apply_dft_to_dft_tmp_bytes); -} -#endif
D spqlios/lib/test/spqlios_zn_approxdecomp_test.cpp

@@ -1,46 +0,0 @@

-#include "gtest/gtest.h" -#include "spqlios/arithmetic/zn_arithmetic_private.h" -#include "testlib/test_commons.h" - -template <typename INTTYPE> -static void test_tndbl_approxdecomp( // - void (*approxdec)(const MOD_Z*, const TNDBL_APPROXDECOMP_GADGET*, INTTYPE*, uint64_t, const double*, uint64_t) // -) { - for (const uint64_t nn : {1, 3, 8, 51}) { - MOD_Z* module = new_z_module_info(DEFAULT); - for (const uint64_t ell : {1, 2, 7}) { - for (const uint64_t k : {2, 5}) { - TNDBL_APPROXDECOMP_GADGET* gadget = new_tndbl_approxdecomp_gadget(module, k, ell); - for (const uint64_t res_size : {ell * nn}) { - std::vector<double> in(nn); - std::vector<INTTYPE> out(res_size); - for (double& x : in) x = uniform_f64_bounds(-10, 10); - approxdec(module, gadget, out.data(), res_size, in.data(), nn); - // reconstruct the output - double err_bnd = pow(2., -double(ell * k) - 1); - for (uint64_t j = 0; j < nn; ++j) { - double in_j = in[j]; - double out_j = 0; - for (uint64_t i = 0; i < ell; ++i) { - out_j += out[ell * j + i] * pow(2., -double((i + 1) * k)); - } - double err = out_j - in_j; - double err_abs = fabs(err - rint(err)); - ASSERT_LE(err_abs, err_bnd); - } - } - delete_tndbl_approxdecomp_gadget(gadget); - } - } - delete_z_module_info(module); - } -} - -TEST(vec_rnx, i8_tndbl_rnx_approxdecomp) { test_tndbl_approxdecomp(i8_approxdecomp_from_tndbl); } -TEST(vec_rnx, default_i8_tndbl_rnx_approxdecomp) { test_tndbl_approxdecomp(default_i8_approxdecomp_from_tndbl_ref); } - -TEST(vec_rnx, i16_tndbl_rnx_approxdecomp) { test_tndbl_approxdecomp(i16_approxdecomp_from_tndbl); } -TEST(vec_rnx, default_i16_tndbl_rnx_approxdecomp) { test_tndbl_approxdecomp(default_i16_approxdecomp_from_tndbl_ref); } - -TEST(vec_rnx, i32_tndbl_rnx_approxdecomp) { test_tndbl_approxdecomp(i32_approxdecomp_from_tndbl); } -TEST(vec_rnx, default_i32_tndbl_rnx_approxdecomp) { test_tndbl_approxdecomp(default_i32_approxdecomp_from_tndbl_ref); }
D spqlios/lib/test/spqlios_zn_conversions_test.cpp

@@ -1,104 +0,0 @@

-#include <gtest/gtest.h> -#include <spqlios/arithmetic/zn_arithmetic_private.h> - -#include "testlib/test_commons.h" - -template <typename SRC_T, typename DST_T> -static void test_conv(void (*conv_f)(const MOD_Z*, DST_T* res, uint64_t res_size, const SRC_T* a, uint64_t a_size), - DST_T (*ideal_conv_f)(SRC_T x), SRC_T (*random_f)()) { - MOD_Z* module = new_z_module_info(DEFAULT); - for (uint64_t a_size : {0, 1, 2, 42}) { - for (uint64_t res_size : {0, 1, 2, 42}) { - for (uint64_t trials = 0; trials < 100; ++trials) { - std::vector<SRC_T> a(a_size); - std::vector<DST_T> res(res_size); - uint64_t msize = std::min(a_size, res_size); - for (SRC_T& x : a) x = random_f(); - conv_f(module, res.data(), res_size, a.data(), a_size); - for (uint64_t i = 0; i < msize; ++i) { - DST_T expect = ideal_conv_f(a[i]); - DST_T actual = res[i]; - ASSERT_EQ(expect, actual); - } - for (uint64_t i = msize; i < res_size; ++i) { - DST_T expect = 0; - SRC_T actual = res[i]; - ASSERT_EQ(expect, actual); - } - } - } - } - delete_z_module_info(module); -} - -static int32_t ideal_dbl_to_tn32(double a) { - double _2p32 = INT64_C(1) << 32; - double a_mod_1 = a - rint(a); - int64_t t = rint(a_mod_1 * _2p32); - return int32_t(t); -} - -static double random_f64_10() { return uniform_f64_bounds(-10, 10); } - -static void test_dbl_to_tn32(DBL_TO_TN32_F dbl_to_tn32_f) { - test_conv(dbl_to_tn32_f, ideal_dbl_to_tn32, random_f64_10); -} - -TEST(zn_arithmetic, dbl_to_tn32) { test_dbl_to_tn32(dbl_to_tn32); } -TEST(zn_arithmetic, dbl_to_tn32_ref) { test_dbl_to_tn32(dbl_to_tn32_ref); } - -static double ideal_tn32_to_dbl(int32_t a) { - const double _2p32 = INT64_C(1) << 32; - return double(a) / _2p32; -} - -static int32_t random_t32() { return uniform_i64_bits(32); } - -static void test_tn32_to_dbl(TN32_TO_DBL_F tn32_to_dbl_f) { test_conv(tn32_to_dbl_f, ideal_tn32_to_dbl, random_t32); } - -TEST(zn_arithmetic, tn32_to_dbl) { test_tn32_to_dbl(tn32_to_dbl); } -TEST(zn_arithmetic, tn32_to_dbl_ref) { test_tn32_to_dbl(tn32_to_dbl_ref); } - -static int32_t ideal_dbl_round_to_i32(double a) { return int32_t(rint(a)); } - -static double random_dbl_explaw_18() { return uniform_f64_bounds(-1., 1.) * pow(2., uniform_u64_bits(6) % 19); } - -static void test_dbl_round_to_i32(DBL_ROUND_TO_I32_F dbl_round_to_i32_f) { - test_conv(dbl_round_to_i32_f, ideal_dbl_round_to_i32, random_dbl_explaw_18); -} - -TEST(zn_arithmetic, dbl_round_to_i32) { test_dbl_round_to_i32(dbl_round_to_i32); } -TEST(zn_arithmetic, dbl_round_to_i32_ref) { test_dbl_round_to_i32(dbl_round_to_i32_ref); } - -static double ideal_i32_to_dbl(int32_t a) { return double(a); } - -static int32_t random_i32_explaw_18() { return uniform_i64_bits(uniform_u64_bits(6) % 19); } - -static void test_i32_to_dbl(I32_TO_DBL_F i32_to_dbl_f) { - test_conv(i32_to_dbl_f, ideal_i32_to_dbl, random_i32_explaw_18); -} - -TEST(zn_arithmetic, i32_to_dbl) { test_i32_to_dbl(i32_to_dbl); } -TEST(zn_arithmetic, i32_to_dbl_ref) { test_i32_to_dbl(i32_to_dbl_ref); } - -static int64_t ideal_dbl_round_to_i64(double a) { return rint(a); } - -static double random_dbl_explaw_50() { return uniform_f64_bounds(-1., 1.) * pow(2., uniform_u64_bits(7) % 51); } - -static void test_dbl_round_to_i64(DBL_ROUND_TO_I64_F dbl_round_to_i64_f) { - test_conv(dbl_round_to_i64_f, ideal_dbl_round_to_i64, random_dbl_explaw_50); -} - -TEST(zn_arithmetic, dbl_round_to_i64) { test_dbl_round_to_i64(dbl_round_to_i64); } -TEST(zn_arithmetic, dbl_round_to_i64_ref) { test_dbl_round_to_i64(dbl_round_to_i64_ref); } - -static double ideal_i64_to_dbl(int64_t a) { return double(a); } - -static int64_t random_i64_explaw_50() { return uniform_i64_bits(uniform_u64_bits(7) % 51); } - -static void test_i64_to_dbl(I64_TO_DBL_F i64_to_dbl_f) { - test_conv(i64_to_dbl_f, ideal_i64_to_dbl, random_i64_explaw_50); -} - -TEST(zn_arithmetic, i64_to_dbl) { test_i64_to_dbl(i64_to_dbl); } -TEST(zn_arithmetic, i64_to_dbl_ref) { test_i64_to_dbl(i64_to_dbl_ref); }
D spqlios/lib/test/spqlios_zn_vmp_test.cpp

@@ -1,67 +0,0 @@

-#include "gtest/gtest.h" -#include "spqlios/arithmetic/zn_arithmetic_private.h" -#include "testlib/zn_layouts.h" - -static void test_zn_vmp_prepare(ZN32_VMP_PREPARE_CONTIGUOUS_F prep) { - MOD_Z* module = new_z_module_info(DEFAULT); - for (uint64_t nrows : {1, 2, 5, 15}) { - for (uint64_t ncols : {1, 2, 32, 42, 67}) { - std::vector<int32_t> src(nrows * ncols); - zn32_pmat_layout out(nrows, ncols); - for (int32_t& x : src) x = uniform_i64_bits(32); - prep(module, out.data, src.data(), nrows, ncols); - for (uint64_t i = 0; i < nrows; ++i) { - for (uint64_t j = 0; j < ncols; ++j) { - int32_t in = src[i * ncols + j]; - int32_t actual = out.get(i, j); - ASSERT_EQ(actual, in); - } - } - } - } - delete_z_module_info(module); -} - -TEST(zn, zn32_vmp_prepare_contiguous) { test_zn_vmp_prepare(zn32_vmp_prepare_contiguous); } -TEST(zn, default_zn32_vmp_prepare_contiguous_ref) { test_zn_vmp_prepare(default_zn32_vmp_prepare_contiguous_ref); } - -template <typename INTTYPE> -static void test_zn_vmp_apply(void (*apply)(const MOD_Z*, int32_t*, uint64_t, const INTTYPE*, uint64_t, - const ZN32_VMP_PMAT*, uint64_t, uint64_t)) { - MOD_Z* module = new_z_module_info(DEFAULT); - for (uint64_t nrows : {1, 2, 5, 15}) { - for (uint64_t ncols : {1, 2, 32, 42, 67}) { - for (uint64_t a_size : {1, 2, 5, 15}) { - for (uint64_t res_size : {1, 2, 32, 42, 67}) { - std::vector<INTTYPE> a(a_size); - zn32_pmat_layout out(nrows, ncols); - std::vector<int32_t> res(res_size); - for (INTTYPE& x : a) x = uniform_i64_bits(32); - out.fill_random(); - std::vector<int32_t> expect = vmp_product(a.data(), a_size, res_size, out); - apply(module, res.data(), res_size, a.data(), a_size, out.data, nrows, ncols); - for (uint64_t i = 0; i < res_size; ++i) { - int32_t exp = expect[i]; - int32_t actual = res[i]; - ASSERT_EQ(actual, exp); - } - } - } - } - } - delete_z_module_info(module); -} - -TEST(zn, zn32_vmp_apply_i32) { test_zn_vmp_apply(zn32_vmp_apply_i32); } -TEST(zn, zn32_vmp_apply_i16) { test_zn_vmp_apply(zn32_vmp_apply_i16); } -TEST(zn, zn32_vmp_apply_i8) { test_zn_vmp_apply(zn32_vmp_apply_i8); } - -TEST(zn, default_zn32_vmp_apply_i32_ref) { test_zn_vmp_apply(default_zn32_vmp_apply_i32_ref); } -TEST(zn, default_zn32_vmp_apply_i16_ref) { test_zn_vmp_apply(default_zn32_vmp_apply_i16_ref); } -TEST(zn, default_zn32_vmp_apply_i8_ref) { test_zn_vmp_apply(default_zn32_vmp_apply_i8_ref); } - -#ifdef __x86_64__ -TEST(zn, default_zn32_vmp_apply_i32_avx) { test_zn_vmp_apply(default_zn32_vmp_apply_i32_avx); } -TEST(zn, default_zn32_vmp_apply_i16_avx) { test_zn_vmp_apply(default_zn32_vmp_apply_i16_avx); } -TEST(zn, default_zn32_vmp_apply_i8_avx) { test_zn_vmp_apply(default_zn32_vmp_apply_i8_avx); } -#endif
D spqlios/lib/test/spqlios_znx_small_test.cpp

@@ -1,26 +0,0 @@

-#include <gtest/gtest.h> - -#include "../spqlios/arithmetic/vec_znx_arithmetic_private.h" -#include "testlib/negacyclic_polynomial.h" - -static void test_znx_small_single_product(ZNX_SMALL_SINGLE_PRODUCT_F product, - ZNX_SMALL_SINGLE_PRODUCT_TMP_BYTES_F product_tmp_bytes) { - for (const uint64_t nn : {2, 4, 8, 64}) { - MODULE* module = new_module_info(nn, FFT64); - znx_i64 a = znx_i64::random_log2bound(nn, 20); - znx_i64 b = znx_i64::random_log2bound(nn, 20); - znx_i64 expect = naive_product(a, b); - znx_i64 actual(nn); - std::vector<uint8_t> tmp(znx_small_single_product_tmp_bytes(module)); - fft64_znx_small_single_product(module, actual.data(), a.data(), b.data(), tmp.data()); - ASSERT_EQ(actual, expect) << actual.get_coeff(0) << " vs. " << expect.get_coeff(0); - delete_module_info(module); - } -} - -TEST(znx_small, fft64_znx_small_single_product) { - test_znx_small_single_product(fft64_znx_small_single_product, fft64_znx_small_single_product_tmp_bytes); -} -TEST(znx_small, znx_small_single_product) { - test_znx_small_single_product(znx_small_single_product, znx_small_single_product_tmp_bytes); -}
D spqlios/lib/test/testlib/fft64_dft.cpp

@@ -1,168 +0,0 @@

-#include "fft64_dft.h" - -#include <cstring> - -#include "../../spqlios/reim/reim_fft.h" -#include "../../spqlios/reim/reim_fft_internal.h" - -reim_fft64vec::reim_fft64vec(uint64_t n) : v(n, 0) {} -reim4_elem reim_fft64vec::get_blk(uint64_t blk) const { - return reim_view(v.size() / 2, (double*)v.data()).get_blk(blk); -} -double* reim_fft64vec::data() { return v.data(); } -const double* reim_fft64vec::data() const { return v.data(); } -uint64_t reim_fft64vec::nn() const { return v.size(); } -reim_fft64vec::reim_fft64vec(uint64_t n, const double* data) : v(data, data + n) {} -void reim_fft64vec::save_as(double* dest) const { memcpy(dest, v.data(), nn() * sizeof(double)); } -reim_fft64vec reim_fft64vec::zero(uint64_t n) { return reim_fft64vec(n); } -void reim_fft64vec::set_blk(uint64_t blk, const reim4_elem& value) { - reim_view(v.size() / 2, (double*)v.data()).set_blk(blk, value); -} -reim_fft64vec reim_fft64vec::dft_random(uint64_t n, uint64_t log2bound) { - return simple_fft64(znx_i64::random_log2bound(n, log2bound)); -} -reim_fft64vec reim_fft64vec::random(uint64_t n, double log2bound) { - double bound = pow(2., log2bound); - reim_fft64vec res(n); - for (uint64_t i = 0; i < n; ++i) { - res.v[i] = uniform_f64_bounds(-bound, bound); - } - return res; -} - -reim_fft64vec operator+(const reim_fft64vec& a, const reim_fft64vec& b) { - uint64_t nn = a.nn(); - REQUIRE_DRAMATICALLY(b.nn() == a.nn(), "ring dimension mismatch"); - reim_fft64vec res(nn); - double* rv = res.data(); - const double* av = a.data(); - const double* bv = b.data(); - for (uint64_t i = 0; i < nn; ++i) { - rv[i] = av[i] + bv[i]; - } - return res; -} -reim_fft64vec operator-(const reim_fft64vec& a, const reim_fft64vec& b) { - uint64_t nn = a.nn(); - REQUIRE_DRAMATICALLY(b.nn() == a.nn(), "ring dimension mismatch"); - reim_fft64vec res(nn); - double* rv = res.data(); - const double* av = a.data(); - const double* bv = b.data(); - for (uint64_t i = 0; i < nn; ++i) { - rv[i] = av[i] - bv[i]; - } - return res; -} -reim_fft64vec operator*(const reim_fft64vec& a, const reim_fft64vec& b) { - uint64_t nn = a.nn(); - REQUIRE_DRAMATICALLY(b.nn() == a.nn(), "ring dimension mismatch"); - REQUIRE_DRAMATICALLY(nn >= 2, "test not defined for nn=1"); - uint64_t m = nn / 2; - reim_fft64vec res(nn); - double* rv = res.data(); - const double* av = a.data(); - const double* bv = b.data(); - for (uint64_t i = 0; i < m; ++i) { - rv[i] = av[i] * bv[i] - av[m + i] * bv[m + i]; - rv[m + i] = av[i] * bv[m + i] + av[m + i] * bv[i]; - } - return res; -} -reim_fft64vec& operator+=(reim_fft64vec& a, const reim_fft64vec& b) { - uint64_t nn = a.nn(); - REQUIRE_DRAMATICALLY(b.nn() == a.nn(), "ring dimension mismatch"); - double* av = a.data(); - const double* bv = b.data(); - for (uint64_t i = 0; i < nn; ++i) { - av[i] = av[i] + bv[i]; - } - return a; -} -reim_fft64vec& operator-=(reim_fft64vec& a, const reim_fft64vec& b) { - uint64_t nn = a.nn(); - REQUIRE_DRAMATICALLY(b.nn() == a.nn(), "ring dimension mismatch"); - double* av = a.data(); - const double* bv = b.data(); - for (uint64_t i = 0; i < nn; ++i) { - av[i] = av[i] - bv[i]; - } - return a; -} - -reim_fft64vec simple_fft64(const znx_i64& polynomial) { - const uint64_t nn = polynomial.nn(); - const uint64_t m = nn / 2; - reim_fft64vec res(nn); - double* dat = res.data(); - for (uint64_t i = 0; i < nn; ++i) dat[i] = polynomial.get_coeff(i); - reim_fft_simple(m, dat); - return res; -} - -znx_i64 simple_rint_ifft64(const reim_fft64vec& fftvec) { - const uint64_t nn = fftvec.nn(); - const uint64_t m = nn / 2; - std::vector<double> vv(fftvec.data(), fftvec.data() + nn); - double* v = vv.data(); - reim_ifft_simple(m, v); - znx_i64 res(nn); - for (uint64_t i = 0; i < nn; ++i) { - res.set_coeff(i, rint(v[i] / m)); - } - return res; -} - -rnx_f64 naive_ifft64(const reim_fft64vec& fftvec) { - const uint64_t nn = fftvec.nn(); - const uint64_t m = nn / 2; - std::vector<double> vv(fftvec.data(), fftvec.data() + nn); - double* v = vv.data(); - reim_ifft_simple(m, v); - rnx_f64 res(nn); - for (uint64_t i = 0; i < nn; ++i) { - res.set_coeff(i, v[i] / m); - } - return res; -} -double infty_dist(const reim_fft64vec& a, const reim_fft64vec& b) { - const uint64_t n = a.nn(); - REQUIRE_DRAMATICALLY(b.nn() == a.nn(), "dimensions mismatch"); - const double* da = a.data(); - const double* db = b.data(); - double d = 0; - for (uint64_t i = 0; i < n; ++i) { - double di = abs(da[i] - db[i]); - if (di > d) d = di; - } - return d; -} - -reim_fft64vec simple_fft64(const rnx_f64& polynomial) { - const uint64_t nn = polynomial.nn(); - const uint64_t m = nn / 2; - reim_fft64vec res(nn); - double* dat = res.data(); - for (uint64_t i = 0; i < nn; ++i) dat[i] = polynomial.get_coeff(i); - reim_fft_simple(m, dat); - return res; -} - -reim_fft64vec operator*(double coeff, const reim_fft64vec& v) { - const uint64_t nn = v.nn(); - reim_fft64vec res(nn); - double* rr = res.data(); - const double* vv = v.data(); - for (uint64_t i = 0; i < nn; ++i) rr[i] = coeff * vv[i]; - return res; -} - -rnx_f64 simple_ifft64(const reim_fft64vec& v) { - const uint64_t nn = v.nn(); - const uint64_t m = nn / 2; - rnx_f64 res(nn); - double* dat = res.data(); - memcpy(dat, v.data(), nn * sizeof(double)); - reim_ifft_simple(m, dat); - return res; -}
D spqlios/lib/test/testlib/fft64_dft.h

@@ -1,43 +0,0 @@

-#ifndef SPQLIOS_FFT64_DFT_H -#define SPQLIOS_FFT64_DFT_H - -#include "negacyclic_polynomial.h" -#include "reim4_elem.h" - -class reim_fft64vec { - std::vector<double> v; - - public: - reim_fft64vec() = default; - explicit reim_fft64vec(uint64_t n); - reim_fft64vec(uint64_t n, const double* data); - uint64_t nn() const; - static reim_fft64vec zero(uint64_t n); - /** random complex coefficients (unstructured) */ - static reim_fft64vec random(uint64_t n, double log2bound); - /** random fft of a small int polynomial */ - static reim_fft64vec dft_random(uint64_t n, uint64_t log2bound); - double* data(); - const double* data() const; - void save_as(double* dest) const; - reim4_elem get_blk(uint64_t blk) const; - void set_blk(uint64_t blk, const reim4_elem& value); -}; - -reim_fft64vec operator+(const reim_fft64vec& a, const reim_fft64vec& b); -reim_fft64vec operator-(const reim_fft64vec& a, const reim_fft64vec& b); -reim_fft64vec operator*(const reim_fft64vec& a, const reim_fft64vec& b); -reim_fft64vec operator*(double coeff, const reim_fft64vec& v); -reim_fft64vec& operator+=(reim_fft64vec& a, const reim_fft64vec& b); -reim_fft64vec& operator-=(reim_fft64vec& a, const reim_fft64vec& b); - -/** infty distance */ -double infty_dist(const reim_fft64vec& a, const reim_fft64vec& b); - -reim_fft64vec simple_fft64(const znx_i64& polynomial); -znx_i64 simple_rint_ifft64(const reim_fft64vec& fftvec); -rnx_f64 naive_ifft64(const reim_fft64vec& fftvec); -reim_fft64vec simple_fft64(const rnx_f64& polynomial); -rnx_f64 simple_ifft64(const reim_fft64vec& v); - -#endif // SPQLIOS_FFT64_DFT_H
D spqlios/lib/test/testlib/fft64_layouts.cpp

@@ -1,238 +0,0 @@

-#include "fft64_layouts.h" -#ifdef VALGRIND_MEM_TESTS -#include "valgrind/memcheck.h" -#endif - -void* alloc64(uint64_t size) { - static uint64_t _msk64 = -64; - if (size == 0) return nullptr; - uint64_t rsize = (size + 63) & _msk64; - uint8_t* reps = (uint8_t*)spqlios_alloc(rsize); - REQUIRE_DRAMATICALLY(reps != 0, "Out of memory"); -#ifdef VALGRIND_MEM_TESTS - VALGRIND_MAKE_MEM_NOACCESS(reps + size, rsize - size); -#endif - return reps; -} - -fft64_vec_znx_dft_layout::fft64_vec_znx_dft_layout(uint64_t n, uint64_t size) - : nn(n), // - size(size), // - data((VEC_ZNX_DFT*)alloc64(n * size * 8)), // - view(n / 2, size, (double*)data) {} - -fft64_vec_znx_dft_layout::~fft64_vec_znx_dft_layout() { spqlios_free(data); } - -double* fft64_vec_znx_dft_layout::get_addr(uint64_t idx) { - REQUIRE_DRAMATICALLY(idx < size, "index overflow " << idx << " / " << size); - return ((double*)data) + idx * nn; -} -const double* fft64_vec_znx_dft_layout::get_addr(uint64_t idx) const { - REQUIRE_DRAMATICALLY(idx < size, "index overflow " << idx << " / " << size); - return ((double*)data) + idx * nn; -} -reim_fft64vec fft64_vec_znx_dft_layout::get_copy_zext(uint64_t idx) const { - if (idx < size) { - return reim_fft64vec(nn, get_addr(idx)); - } else { - return reim_fft64vec::zero(nn); - } -} -void fft64_vec_znx_dft_layout::fill_dft_random_log2bound(uint64_t bits) { - for (uint64_t i = 0; i < size; ++i) { - set(i, simple_fft64(znx_i64::random_log2bound(nn, bits))); - } -} -void fft64_vec_znx_dft_layout::set(uint64_t idx, const reim_fft64vec& value) { - REQUIRE_DRAMATICALLY(value.nn() == nn, "ring dimension mismatch"); - value.save_as(get_addr(idx)); -} -thash fft64_vec_znx_dft_layout::content_hash() const { return test_hash(data, size * nn * sizeof(double)); } - -reim4_elem fft64_vec_znx_dft_layout::get(uint64_t idx, uint64_t blk) const { - REQUIRE_DRAMATICALLY(idx < size, "index overflow: " << idx << " / " << size); - REQUIRE_DRAMATICALLY(blk < nn / 8, "blk overflow: " << blk << " / " << nn / 8); - double* reim = ((double*)data) + idx * nn; - return reim4_elem(reim + blk * 4, reim + nn / 2 + blk * 4); -} -reim4_elem fft64_vec_znx_dft_layout::get_zext(uint64_t idx, uint64_t blk) const { - REQUIRE_DRAMATICALLY(blk < nn / 8, "blk overflow: " << blk << " / " << nn / 8); - if (idx < size) { - return get(idx, blk); - } else { - return reim4_elem::zero(); - } -} -void fft64_vec_znx_dft_layout::set(uint64_t idx, uint64_t blk, const reim4_elem& value) { - REQUIRE_DRAMATICALLY(idx < size, "index overflow: " << idx << " / " << size); - REQUIRE_DRAMATICALLY(blk < nn / 8, "blk overflow: " << blk << " / " << nn / 8); - double* reim = ((double*)data) + idx * nn; - value.save_re_im(reim + blk * 4, reim + nn / 2 + blk * 4); -} -void fft64_vec_znx_dft_layout::fill_random(double log2bound) { - for (uint64_t i = 0; i < size; ++i) { - set(i, reim_fft64vec::random(nn, log2bound)); - } -} -void fft64_vec_znx_dft_layout::fill_dft_random(uint64_t log2bound) { - for (uint64_t i = 0; i < size; ++i) { - set(i, reim_fft64vec::dft_random(nn, log2bound)); - } -} - -fft64_vec_znx_big_layout::fft64_vec_znx_big_layout(uint64_t n, uint64_t size) - : nn(n), // - size(size), // - data((VEC_ZNX_BIG*)alloc64(n * size * 8)) {} - -znx_i64 fft64_vec_znx_big_layout::get_copy(uint64_t index) const { - REQUIRE_DRAMATICALLY(index < size, "index overflow: " << index << " / " << size); - return znx_i64(nn, ((int64_t*)data) + index * nn); -} -znx_i64 fft64_vec_znx_big_layout::get_copy_zext(uint64_t index) const { - if (index < size) { - return znx_i64(nn, ((int64_t*)data) + index * nn); - } else { - return znx_i64::zero(nn); - } -} -void fft64_vec_znx_big_layout::set(uint64_t index, const znx_i64& value) { - REQUIRE_DRAMATICALLY(index < size, "index overflow: " << index << " / " << size); - value.save_as(((int64_t*)data) + index * nn); -} -void fft64_vec_znx_big_layout::fill_random() { - for (uint64_t i = 0; i < size; ++i) { - set(i, znx_i64::random_log2bound(nn, 1)); - } -} -fft64_vec_znx_big_layout::~fft64_vec_znx_big_layout() { spqlios_free(data); } - -fft64_vmp_pmat_layout::fft64_vmp_pmat_layout(uint64_t n, uint64_t nrows, uint64_t ncols) - : nn(n), - nrows(nrows), - ncols(ncols), // - data((VMP_PMAT*)alloc64(nrows * ncols * nn * 8)) {} - -double* fft64_vmp_pmat_layout::get_addr(uint64_t row, uint64_t col, uint64_t blk) const { - REQUIRE_DRAMATICALLY(row < nrows, "row overflow: " << row << " / " << nrows); - REQUIRE_DRAMATICALLY(col < ncols, "col overflow: " << col << " / " << ncols); - REQUIRE_DRAMATICALLY(blk < nn / 8, "block overflow: " << blk << " / " << (nn / 8)); - double* d = (double*)data; - if (col == (ncols - 1) && (ncols % 2 == 1)) { - // special case: last column out of an odd column number - return d + blk * nrows * ncols * 8 // major: blk - + col * nrows * 8 // col == ncols-1 - + row * 8; - } else { - // general case: columns go by pair - return d + blk * nrows * ncols * 8 // major: blk - + (col / 2) * (2 * nrows) * 8 // second: col pair index - + row * 2 * 8 // third: row index - + (col % 2) * 8; // minor: col in colpair - } -} - -reim4_elem fft64_vmp_pmat_layout::get(uint64_t row, uint64_t col, uint64_t blk) const { - return reim4_elem(get_addr(row, col, blk)); -} -reim4_elem fft64_vmp_pmat_layout::get_zext(uint64_t row, uint64_t col, uint64_t blk) const { - REQUIRE_DRAMATICALLY(blk < nn / 8, "block overflow: " << blk << " / " << (nn / 8)); - if (row < nrows && col < ncols) { - return reim4_elem(get_addr(row, col, blk)); - } else { - return reim4_elem::zero(); - } -} -void fft64_vmp_pmat_layout::set(uint64_t row, uint64_t col, uint64_t blk, const reim4_elem& value) const { - value.save_as(get_addr(row, col, blk)); -} - -fft64_vmp_pmat_layout::~fft64_vmp_pmat_layout() { spqlios_free(data); } - -reim_fft64vec fft64_vmp_pmat_layout::get_zext(uint64_t row, uint64_t col) const { - if (row >= nrows || col >= ncols) { - return reim_fft64vec::zero(nn); - } - if (nn < 8) { - // the pmat is just col major - double* addr = (double*)data + (row + col * nrows) * nn; - return reim_fft64vec(nn, addr); - } - // otherwise, reconstruct it block by block - reim_fft64vec res(nn); - for (uint64_t blk = 0; blk < nn / 8; ++blk) { - reim4_elem v = get(row, col, blk); - res.set_blk(blk, v); - } - return res; -} -void fft64_vmp_pmat_layout::set(uint64_t row, uint64_t col, const reim_fft64vec& value) { - REQUIRE_DRAMATICALLY(row < nrows, "row overflow: " << row << " / " << nrows); - REQUIRE_DRAMATICALLY(col < ncols, "row overflow: " << col << " / " << ncols); - if (nn < 8) { - // the pmat is just col major - double* addr = (double*)data + (row + col * nrows) * nn; - value.save_as(addr); - return; - } - // otherwise, reconstruct it block by block - for (uint64_t blk = 0; blk < nn / 8; ++blk) { - reim4_elem v = value.get_blk(blk); - set(row, col, blk, v); - } -} -void fft64_vmp_pmat_layout::fill_random(double log2bound) { - for (uint64_t row = 0; row < nrows; ++row) { - for (uint64_t col = 0; col < ncols; ++col) { - set(row, col, reim_fft64vec::random(nn, log2bound)); - } - } -} -void fft64_vmp_pmat_layout::fill_dft_random(uint64_t log2bound) { - for (uint64_t row = 0; row < nrows; ++row) { - for (uint64_t col = 0; col < ncols; ++col) { - set(row, col, reim_fft64vec::dft_random(nn, log2bound)); - } - } -} - -fft64_svp_ppol_layout::fft64_svp_ppol_layout(uint64_t n) - : nn(n), // - data((SVP_PPOL*)alloc64(nn * 8)) {} - -reim_fft64vec fft64_svp_ppol_layout::get_copy() const { return reim_fft64vec(nn, (double*)data); } - -void fft64_svp_ppol_layout::set(const reim_fft64vec& value) { value.save_as((double*)data); } - -void fft64_svp_ppol_layout::fill_dft_random(uint64_t log2bound) { set(reim_fft64vec::dft_random(nn, log2bound)); } - -void fft64_svp_ppol_layout::fill_random(double log2bound) { set(reim_fft64vec::random(nn, log2bound)); } - -fft64_svp_ppol_layout::~fft64_svp_ppol_layout() { spqlios_free(data); } -thash fft64_svp_ppol_layout::content_hash() const { return test_hash(data, nn * sizeof(double)); } - -fft64_cnv_left_layout::fft64_cnv_left_layout(uint64_t n, uint64_t size) - : nn(n), // - size(size), - data((CNV_PVEC_L*)alloc64(size * nn * 8)) {} - -reim4_elem fft64_cnv_left_layout::get(uint64_t idx, uint64_t blk) { - REQUIRE_DRAMATICALLY(idx < size, "idx overflow: " << idx << " / " << size); - REQUIRE_DRAMATICALLY(blk < nn / 8, "block overflow: " << blk << " / " << (nn / 8)); - return reim4_elem(((double*)data) + blk * size + idx); -} - -fft64_cnv_left_layout::~fft64_cnv_left_layout() { spqlios_free(data); } - -fft64_cnv_right_layout::fft64_cnv_right_layout(uint64_t n, uint64_t size) - : nn(n), // - size(size), - data((CNV_PVEC_R*)alloc64(size * nn * 8)) {} - -reim4_elem fft64_cnv_right_layout::get(uint64_t idx, uint64_t blk) { - REQUIRE_DRAMATICALLY(idx < size, "idx overflow: " << idx << " / " << size); - REQUIRE_DRAMATICALLY(blk < nn / 8, "block overflow: " << blk << " / " << (nn / 8)); - return reim4_elem(((double*)data) + blk * size + idx); -} - -fft64_cnv_right_layout::~fft64_cnv_right_layout() { spqlios_free(data); }
D spqlios/lib/test/testlib/fft64_layouts.h

@@ -1,109 +0,0 @@

-#ifndef SPQLIOS_FFT64_LAYOUTS_H -#define SPQLIOS_FFT64_LAYOUTS_H - -#include "../../spqlios/arithmetic/vec_znx_arithmetic.h" -#include "fft64_dft.h" -#include "negacyclic_polynomial.h" -#include "reim4_elem.h" - -/** @brief test layout for the VEC_ZNX_DFT */ -struct fft64_vec_znx_dft_layout { - public: - const uint64_t nn; - const uint64_t size; - VEC_ZNX_DFT* const data; - reim_vector_view view; - /** @brief fill with random double values (unstructured) */ - void fill_random(double log2bound); - /** @brief fill with random ffts of small int polynomials */ - void fill_dft_random(uint64_t log2bound); - reim4_elem get(uint64_t idx, uint64_t blk) const; - reim4_elem get_zext(uint64_t idx, uint64_t blk) const; - void set(uint64_t idx, uint64_t blk, const reim4_elem& value); - fft64_vec_znx_dft_layout(uint64_t n, uint64_t size); - void fill_random_log2bound(uint64_t bits); - void fill_dft_random_log2bound(uint64_t bits); - double* get_addr(uint64_t idx); - const double* get_addr(uint64_t idx) const; - reim_fft64vec get_copy_zext(uint64_t idx) const; - void set(uint64_t idx, const reim_fft64vec& value); - thash content_hash() const; - ~fft64_vec_znx_dft_layout(); -}; - -/** @brief test layout for the VEC_ZNX_BIG */ -class fft64_vec_znx_big_layout { - public: - const uint64_t nn; - const uint64_t size; - VEC_ZNX_BIG* const data; - fft64_vec_znx_big_layout(uint64_t n, uint64_t size); - void fill_random(); - znx_i64 get_copy(uint64_t index) const; - znx_i64 get_copy_zext(uint64_t index) const; - void set(uint64_t index, const znx_i64& value); - thash content_hash() const; - ~fft64_vec_znx_big_layout(); -}; - -/** @brief test layout for the VMP_PMAT */ -class fft64_vmp_pmat_layout { - public: - const uint64_t nn; - const uint64_t nrows; - const uint64_t ncols; - VMP_PMAT* const data; - fft64_vmp_pmat_layout(uint64_t n, uint64_t nrows, uint64_t ncols); - double* get_addr(uint64_t row, uint64_t col, uint64_t blk) const; - reim4_elem get(uint64_t row, uint64_t col, uint64_t blk) const; - thash content_hash() const; - reim4_elem get_zext(uint64_t row, uint64_t col, uint64_t blk) const; - reim_fft64vec get_zext(uint64_t row, uint64_t col) const; - void set(uint64_t row, uint64_t col, uint64_t blk, const reim4_elem& v) const; - void set(uint64_t row, uint64_t col, const reim_fft64vec& value); - /** @brief fill with random double values (unstructured) */ - void fill_random(double log2bound); - /** @brief fill with random ffts of small int polynomials */ - void fill_dft_random(uint64_t log2bound); - ~fft64_vmp_pmat_layout(); -}; - -/** @brief test layout for the SVP_PPOL */ -class fft64_svp_ppol_layout { - public: - const uint64_t nn; - SVP_PPOL* const data; - fft64_svp_ppol_layout(uint64_t n); - thash content_hash() const; - reim_fft64vec get_copy() const; - void set(const reim_fft64vec&); - /** @brief fill with random double values (unstructured) */ - void fill_random(double log2bound); - /** @brief fill with random ffts of small int polynomials */ - void fill_dft_random(uint64_t log2bound); - ~fft64_svp_ppol_layout(); -}; - -/** @brief test layout for the CNV_PVEC_L */ -class fft64_cnv_left_layout { - const uint64_t nn; - const uint64_t size; - CNV_PVEC_L* const data; - fft64_cnv_left_layout(uint64_t n, uint64_t size); - reim4_elem get(uint64_t idx, uint64_t blk); - thash content_hash() const; - ~fft64_cnv_left_layout(); -}; - -/** @brief test layout for the CNV_PVEC_R */ -class fft64_cnv_right_layout { - const uint64_t nn; - const uint64_t size; - CNV_PVEC_R* const data; - fft64_cnv_right_layout(uint64_t n, uint64_t size); - reim4_elem get(uint64_t idx, uint64_t blk); - thash content_hash() const; - ~fft64_cnv_right_layout(); -}; - -#endif // SPQLIOS_FFT64_LAYOUTS_H
D spqlios/lib/test/testlib/mod_q120.cpp

@@ -1,229 +0,0 @@

-#include "mod_q120.h" - -#include <cstdint> -#include <random> - -int64_t centermod(int64_t v, int64_t q) { - int64_t t = v % q; - if (t >= (q + 1) / 2) return t - q; - if (t < -q / 2) return t + q; - return t; -} - -int64_t centermod(uint64_t v, int64_t q) { - int64_t t = int64_t(v % uint64_t(q)); - if (t >= q / 2) return t - q; - return t; -} - -mod_q120::mod_q120() { - for (uint64_t i = 0; i < 4; ++i) { - a[i] = 0; - } -} - -mod_q120::mod_q120(int64_t a0, int64_t a1, int64_t a2, int64_t a3) { - a[0] = centermod(a0, Qi[0]); - a[1] = centermod(a1, Qi[1]); - a[2] = centermod(a2, Qi[2]); - a[3] = centermod(a3, Qi[3]); -} - -mod_q120 operator+(const mod_q120& x, const mod_q120& y) { - mod_q120 r; - for (uint64_t i = 0; i < 4; ++i) { - r.a[i] = centermod(x.a[i] + y.a[i], mod_q120::Qi[i]); - } - return r; -} - -mod_q120 operator-(const mod_q120& x, const mod_q120& y) { - mod_q120 r; - for (uint64_t i = 0; i < 4; ++i) { - r.a[i] = centermod(x.a[i] - y.a[i], mod_q120::Qi[i]); - } - return r; -} - -mod_q120 operator*(const mod_q120& x, const mod_q120& y) { - mod_q120 r; - for (uint64_t i = 0; i < 4; ++i) { - r.a[i] = centermod(x.a[i] * y.a[i], mod_q120::Qi[i]); - } - return r; -} - -mod_q120& operator+=(mod_q120& x, const mod_q120& y) { - for (uint64_t i = 0; i < 4; ++i) { - x.a[i] = centermod(x.a[i] + y.a[i], mod_q120::Qi[i]); - } - return x; -} - -mod_q120& operator-=(mod_q120& x, const mod_q120& y) { - for (uint64_t i = 0; i < 4; ++i) { - x.a[i] = centermod(x.a[i] - y.a[i], mod_q120::Qi[i]); - } - return x; -} - -mod_q120& operator*=(mod_q120& x, const mod_q120& y) { - for (uint64_t i = 0; i < 4; ++i) { - x.a[i] = centermod(x.a[i] * y.a[i], mod_q120::Qi[i]); - } - return x; -} - -int64_t modq_pow(int64_t x, int32_t k, int64_t q) { - k = (k % (q - 1) + q - 1) % (q - 1); - - int64_t res = 1; - int64_t x_pow = centermod(x, q); - while (k != 0) { - if (k & 1) res = centermod(res * x_pow, q); - x_pow = centermod(x_pow * x_pow, q); - k >>= 1; - } - return res; -} - -mod_q120 pow(const mod_q120& x, int32_t k) { - const int64_t r0 = modq_pow(x.a[0], k, x.Qi[0]); - const int64_t r1 = modq_pow(x.a[1], k, x.Qi[1]); - const int64_t r2 = modq_pow(x.a[2], k, x.Qi[2]); - const int64_t r3 = modq_pow(x.a[3], k, x.Qi[3]); - return mod_q120{r0, r1, r2, r3}; -} - -static int64_t half_modq(int64_t x, int64_t q) { - // q must be odd in this function - if (x % 2 == 0) return x / 2; - return centermod((x + q) / 2, q); -} - -mod_q120 half(const mod_q120& x) { - const int64_t r0 = half_modq(x.a[0], x.Qi[0]); - const int64_t r1 = half_modq(x.a[1], x.Qi[1]); - const int64_t r2 = half_modq(x.a[2], x.Qi[2]); - const int64_t r3 = half_modq(x.a[3], x.Qi[3]); - return mod_q120{r0, r1, r2, r3}; -} - -bool operator==(const mod_q120& x, const mod_q120& y) { - for (uint64_t i = 0; i < 4; ++i) { - if (x.a[i] != y.a[i]) return false; - } - return true; -} - -std::ostream& operator<<(std::ostream& out, const mod_q120& x) { - return out << "q120{" << x.a[0] << "," << x.a[1] << "," << x.a[2] << "," << x.a[3] << "}"; -} - -mod_q120 mod_q120::from_q120a(const void* addr) { - static const uint64_t _2p32 = UINT64_C(1) << 32; - const uint64_t* in = (const uint64_t*)addr; - mod_q120 r; - for (uint64_t i = 0; i < 4; ++i) { - REQUIRE_DRAMATICALLY(in[i] < _2p32, "invalid layout a q120"); - r.a[i] = centermod(in[i], mod_q120::Qi[i]); - } - return r; -} - -mod_q120 mod_q120::from_q120b(const void* addr) { - const uint64_t* in = (const uint64_t*)addr; - mod_q120 r; - for (uint64_t i = 0; i < 4; ++i) { - r.a[i] = centermod(in[i], mod_q120::Qi[i]); - } - return r; -} - -mod_q120 mod_q120::from_q120c(const void* addr) { - //static const uint64_t _mask_2p32 = (uint64_t(1) << 32) - 1; - const uint32_t* in = (const uint32_t*)addr; - mod_q120 r; - for (uint64_t i = 0, k = 0; i < 8; i += 2, ++k) { - const uint64_t q = mod_q120::Qi[k]; - uint64_t u = in[i]; - uint64_t w = in[i + 1]; - REQUIRE_DRAMATICALLY(((u << 32) % q) == (w % q), - "invalid layout q120c: " << u << ".2^32 != " << (w >> 32) << " mod " << q); - r.a[k] = centermod(u, q); - } - return r; -} -__int128_t mod_q120::to_int128() const { - static const __int128_t qm[] = {(__int128_t(Qi[1]) * Qi[2]) * Qi[3], (__int128_t(Qi[0]) * Qi[2]) * Qi[3], - (__int128_t(Qi[0]) * Qi[1]) * Qi[3], (__int128_t(Qi[0]) * Qi[1]) * Qi[2]}; - static const int64_t CRTi[] = {Q1_CRT_CST, Q2_CRT_CST, Q3_CRT_CST, Q4_CRT_CST}; - static const __int128_t q = qm[0] * Qi[0]; - static const __int128_t qs2 = q / 2; - __int128_t res = 0; - for (uint64_t i = 0; i < 4; ++i) { - res += (a[i] * CRTi[i] % Qi[i]) * qm[i]; - } - res = (((res % q) + q + qs2) % q) - qs2; // centermod - return res; -} -void mod_q120::save_as_q120a(void* dest) const { - int64_t* d = (int64_t*)dest; - for (uint64_t i = 0; i < 4; ++i) { - d[i] = a[i] + Qi[i]; - } -} -void mod_q120::save_as_q120b(void* dest) const { - int64_t* d = (int64_t*)dest; - for (uint64_t i = 0; i < 4; ++i) { - d[i] = a[i] + (Qi[i] * (1 + uniform_u64_bits(32))); - } -} -void mod_q120::save_as_q120c(void* dest) const { - int32_t* d = (int32_t*)dest; - for (uint64_t i = 0; i < 4; ++i) { - d[2 * i] = a[i] + 3 * Qi[i]; - d[2 * i + 1] = (uint64_t(d[2 * i]) << 32) % uint64_t(Qi[i]); - } -} - -mod_q120 uniform_q120() { - test_rng& gen = randgen(); - std::uniform_int_distribution<uint64_t> dista(0, mod_q120::Qi[0]); - std::uniform_int_distribution<uint64_t> distb(0, mod_q120::Qi[1]); - std::uniform_int_distribution<uint64_t> distc(0, mod_q120::Qi[2]); - std::uniform_int_distribution<uint64_t> distd(0, mod_q120::Qi[3]); - return mod_q120(dista(gen), distb(gen), distc(gen), distd(gen)); -} - -void uniform_q120a(void* dest) { - uint64_t* res = (uint64_t*)dest; - for (uint64_t i = 0; i < 4; ++i) { - res[i] = uniform_u64_bits(32); - } -} - -void uniform_q120b(void* dest) { - uint64_t* res = (uint64_t*)dest; - for (uint64_t i = 0; i < 4; ++i) { - res[i] = uniform_u64(); - } -} - -void uniform_q120c(void* dest) { - uint32_t* res = (uint32_t*)dest; - static const uint64_t _2p32 = uint64_t(1) << 32; - for (uint64_t i = 0, k = 0; i < 8; i += 2, ++k) { - const uint64_t q = mod_q120::Qi[k]; - const uint64_t z = uniform_u64_bits(32); - const uint64_t z_pow_red = (z << 32) % q; - const uint64_t room = (_2p32 - z_pow_red) / q; - const uint64_t z_pow = z_pow_red + (uniform_u64() % room) * q; - REQUIRE_DRAMATICALLY(z < _2p32, "bug!"); - REQUIRE_DRAMATICALLY(z_pow < _2p32, "bug!"); - REQUIRE_DRAMATICALLY(z_pow % q == (z << 32) % q, "bug!"); - - res[i] = (uint32_t)z; - res[i + 1] = (uint32_t)z_pow; - } -}
D spqlios/lib/test/testlib/mod_q120.h

@@ -1,49 +0,0 @@

-#ifndef SPQLIOS_MOD_Q120_H -#define SPQLIOS_MOD_Q120_H - -#include <cstdint> - -#include "../../spqlios/q120/q120_common.h" -#include "test_commons.h" - -/** @brief centered modulo q */ -int64_t centermod(int64_t v, int64_t q); -int64_t centermod(uint64_t v, int64_t q); - -/** @brief this class represents an integer mod Q120 */ -class mod_q120 { - public: - static constexpr int64_t Qi[] = {Q1, Q2, Q3, Q4}; - int64_t a[4]; - mod_q120(int64_t a1, int64_t a2, int64_t a3, int64_t a4); - mod_q120(); - __int128_t to_int128() const; - static mod_q120 from_q120a(const void* addr); - static mod_q120 from_q120b(const void* addr); - static mod_q120 from_q120c(const void* addr); - void save_as_q120a(void* dest) const; - void save_as_q120b(void* dest) const; - void save_as_q120c(void* dest) const; -}; - -mod_q120 operator+(const mod_q120& x, const mod_q120& y); -mod_q120 operator-(const mod_q120& x, const mod_q120& y); -mod_q120 operator*(const mod_q120& x, const mod_q120& y); -mod_q120& operator+=(mod_q120& x, const mod_q120& y); -mod_q120& operator-=(mod_q120& x, const mod_q120& y); -mod_q120& operator*=(mod_q120& x, const mod_q120& y); -std::ostream& operator<<(std::ostream& out, const mod_q120& x); -bool operator==(const mod_q120& x, const mod_q120& y); -mod_q120 pow(const mod_q120& x, int32_t k); -mod_q120 half(const mod_q120& x); - -/** @brief a uniformly drawn number mod Q120 */ -mod_q120 uniform_q120(); -/** @brief a uniformly random mod Q120 layout A (4 integers < 2^32) */ -void uniform_q120a(void* dest); -/** @brief a uniformly random mod Q120 layout B (4 integers < 2^64) */ -void uniform_q120b(void* dest); -/** @brief a uniformly random mod Q120 layout C (4 integers repr. x,2^32x) */ -void uniform_q120c(void* dest); - -#endif // SPQLIOS_MOD_Q120_H
D spqlios/lib/test/testlib/negacyclic_polynomial.cpp

@@ -1,18 +0,0 @@

-#include "negacyclic_polynomial_impl.h" - -// explicit instantiation -EXPLICIT_INSTANTIATE_POLYNOMIAL(__int128_t); -EXPLICIT_INSTANTIATE_POLYNOMIAL(int64_t); -EXPLICIT_INSTANTIATE_POLYNOMIAL(double); - -double infty_dist(const rnx_f64& a, const rnx_f64& b) { - const uint64_t nn = a.nn(); - const double* aa = a.data(); - const double* bb = b.data(); - double res = 0.; - for (uint64_t i = 0; i < nn; ++i) { - double d = fabs(aa[i] - bb[i]); - if (d > res) res = d; - } - return res; -}
D spqlios/lib/test/testlib/negacyclic_polynomial.h

@@ -1,69 +0,0 @@

-#ifndef SPQLIOS_NEGACYCLIC_POLYNOMIAL_H -#define SPQLIOS_NEGACYCLIC_POLYNOMIAL_H - -#include <cstdint> - -#include "test_commons.h" - -template <typename T> -class polynomial; -typedef polynomial<__int128_t> znx_i128; -typedef polynomial<int64_t> znx_i64; -typedef polynomial<double> rnx_f64; - -template <typename T> -class polynomial { - public: - std::vector<T> coeffs; - /** @brief create a polynomial out of existing coeffs */ - polynomial(uint64_t N, const T* c); - /** @brief zero polynomial of dimension N */ - explicit polynomial(uint64_t N); - /** @brief empty polynomial (dim 0) */ - polynomial(); - - /** @brief ring dimension */ - uint64_t nn() const; - /** @brief special setter (accept any indexes, and does the negacyclic translation) */ - void set_coeff(int64_t i, T v); - /** @brief special getter (accept any indexes, and does the negacyclic translation) */ - T get_coeff(int64_t i) const; - /** @brief returns the coefficient layout */ - T* data(); - /** @brief returns the coefficient layout (const version) */ - const T* data() const; - /** @brief saves to the layout */ - void save_as(T* dest) const; - /** @brief zero */ - static polynomial<T> zero(uint64_t n); - /** @brief random polynomial with coefficients in [-2^log2bounds, 2^log2bounds]*/ - static polynomial<T> random_log2bound(uint64_t n, uint64_t log2bound); - /** @brief random polynomial with coefficients in [-2^log2bounds, 2^log2bounds]*/ - static polynomial<T> random(uint64_t n); - /** @brief random polynomial with coefficient in [lb;ub] */ - static polynomial<T> random_bound(uint64_t n, const T lb, const T ub); -}; - -/** @brief equality operator (used during tests) */ -template <typename T> -bool operator==(const polynomial<T>& a, const polynomial<T>& b); - -/** @brief addition operator (used during tests) */ -template <typename T> -polynomial<T> operator+(const polynomial<T>& a, const polynomial<T>& b); - -/** @brief subtraction operator (used during tests) */ -template <typename T> -polynomial<T> operator-(const polynomial<T>& a, const polynomial<T>& b); - -/** @brief negation operator (used during tests) */ -template <typename T> -polynomial<T> operator-(const polynomial<T>& a); - -template <typename T> -polynomial<T> naive_product(const polynomial<T>& a, const polynomial<T>& b); - -/** @brief distance between two real polynomials (used during tests) */ -double infty_dist(const rnx_f64& a, const rnx_f64& b); - -#endif // SPQLIOS_NEGACYCLIC_POLYNOMIAL_H
D spqlios/lib/test/testlib/negacyclic_polynomial_impl.h

@@ -1,247 +0,0 @@

-#ifndef SPQLIOS_NEGACYCLIC_POLYNOMIAL_IMPL_H -#define SPQLIOS_NEGACYCLIC_POLYNOMIAL_IMPL_H - -#include "negacyclic_polynomial.h" - -template <typename T> -polynomial<T>::polynomial(uint64_t N, const T* c) : coeffs(N) { - for (uint64_t i = 0; i < N; ++i) coeffs[i] = c[i]; -} -/** @brief zero polynomial of dimension N */ -template <typename T> -polynomial<T>::polynomial(uint64_t N) : coeffs(N, 0) {} -/** @brief empty polynomial (dim 0) */ -template <typename T> -polynomial<T>::polynomial() {} - -/** @brief ring dimension */ -template <typename T> -uint64_t polynomial<T>::nn() const { - uint64_t n = coeffs.size(); - REQUIRE_DRAMATICALLY(is_pow2(n), "polynomial dim is not a pow of 2"); - return n; -} - -/** @brief special setter (accept any indexes, and does the negacyclic translation) */ -template <typename T> -void polynomial<T>::set_coeff(int64_t i, T v) { - const uint64_t n = nn(); - const uint64_t _2nm = 2 * n - 1; - uint64_t pos = uint64_t(i) & _2nm; - if (pos < n) { - coeffs[pos] = v; - } else { - coeffs[pos - n] = -v; - } -} -/** @brief special getter (accept any indexes, and does the negacyclic translation) */ -template <typename T> -T polynomial<T>::get_coeff(int64_t i) const { - const uint64_t n = nn(); - const uint64_t _2nm = 2 * n - 1; - uint64_t pos = uint64_t(i) & _2nm; - if (pos < n) { - return coeffs[pos]; - } else { - return -coeffs[pos - n]; - } -} -/** @brief returns the coefficient layout */ -template <typename T> -T* polynomial<T>::data() { - return coeffs.data(); -} - -template <typename T> -void polynomial<T>::save_as(T* dest) const { - const uint64_t n = nn(); - for (uint64_t i = 0; i < n; ++i) { - dest[i] = coeffs[i]; - } -} - -/** @brief returns the coefficient layout (const version) */ -template <typename T> -const T* polynomial<T>::data() const { - return coeffs.data(); -} - -/** @brief returns the coefficient layout (const version) */ -template <typename T> -polynomial<T> polynomial<T>::zero(uint64_t n) { - return polynomial<T>(n); -} - -/** @brief equality operator (used during tests) */ -template <typename T> -bool operator==(const polynomial<T>& a, const polynomial<T>& b) { - uint64_t n = a.nn(); - REQUIRE_DRAMATICALLY(b.nn() == n, "wrong dimensions"); - for (uint64_t i = 0; i < n; ++i) { - if (a.get_coeff(i) != b.get_coeff(i)) return false; - } - return true; -} - -/** @brief addition operator (used during tests) */ -template <typename T> -polynomial<T> operator+(const polynomial<T>& a, const polynomial<T>& b) { - uint64_t n = a.nn(); - REQUIRE_DRAMATICALLY(b.nn() == n, "wrong dimensions"); - polynomial<T> res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, a.get_coeff(i) + b.get_coeff(i)); - } - return res; -} - -/** @brief subtraction operator (used during tests) */ -template <typename T> -polynomial<T> operator-(const polynomial<T>& a, const polynomial<T>& b) { - uint64_t n = a.nn(); - REQUIRE_DRAMATICALLY(b.nn() == n, "wrong dimensions"); - polynomial<T> res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, a.get_coeff(i) - b.get_coeff(i)); - } - return res; -} - -/** @brief subtraction operator (used during tests) */ -template <typename T> -polynomial<T> operator-(const polynomial<T>& a) { - uint64_t n = a.nn(); - polynomial<T> res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, -a.get_coeff(i)); - } - return res; -} - -/** @brief random polynomial */ -template <typename T> -polynomial<T> random_polynomial(uint64_t n); - -/** @brief random int64 polynomial */ -template <> -polynomial<int64_t> random_polynomial(uint64_t n) { - polynomial<int64_t> res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, uniform_i64()); - } - return res; -} - -/** @brief random float64 gaussian polynomial */ -template <> -polynomial<double> random_polynomial(uint64_t n) { - polynomial<double> res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, random_f64_gaussian()); - } - return res; -} - -template <typename T> -polynomial<T> random_polynomial_bounds(uint64_t n, const T lb, const T ub); - -/** @brief random int64 polynomial */ -template <> -polynomial<int64_t> random_polynomial_bounds(uint64_t n, const int64_t lb, const int64_t ub) { - polynomial<int64_t> res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, uniform_i64_bounds(lb, ub)); - } - return res; -} - -/** @brief random float64 gaussian polynomial */ -template <> -polynomial<double> random_polynomial_bounds(uint64_t n, const double lb, const double ub) { - polynomial<double> res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, uniform_f64_bounds(lb, ub)); - } - return res; -} - -/** @brief random int64 polynomial */ -template <> -polynomial<__int128_t> random_polynomial_bounds(uint64_t n, const __int128_t lb, const __int128_t ub) { - polynomial<__int128_t> res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, uniform_i128_bounds(lb, ub)); - } - return res; -} - -template <typename T> -polynomial<T> random_polynomial_bits(uint64_t n, const uint64_t bits) { - T b = UINT64_C(1) << bits; - return random_polynomial_bounds(n, -b, b); -} - -template <> -polynomial<int64_t> polynomial<int64_t>::random_log2bound(uint64_t n, uint64_t log2bound) { - polynomial<int64_t> res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, uniform_i64_bits(log2bound)); - } - return res; -} - -template <> -polynomial<int64_t> polynomial<int64_t>::random(uint64_t n) { - polynomial<int64_t> res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, uniform_u64()); - } - return res; -} - -template <> -polynomial<double> polynomial<double>::random_log2bound(uint64_t n, uint64_t log2bound) { - polynomial<double> res(n); - double bound = pow(2., log2bound); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, uniform_f64_bounds(-bound, bound)); - } - return res; -} - -template <> -polynomial<double> polynomial<double>::random(uint64_t n) { - polynomial<double> res(n); - double bound = 2.; - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, uniform_f64_bounds(-bound, bound)); - } - return res; -} - -template <typename T> -polynomial<T> naive_product(const polynomial<T>& a, const polynomial<T>& b) { - const int64_t nn = a.nn(); - REQUIRE_DRAMATICALLY(b.nn() == uint64_t(nn), "dimension mismatch!"); - polynomial<T> res(nn); - for (int64_t i = 0; i < nn; ++i) { - T ri = 0; - for (int64_t j = 0; j < nn; ++j) { - ri += a.get_coeff(j) * b.get_coeff(i - j); - } - res.set_coeff(i, ri); - } - return res; -} - -#define EXPLICIT_INSTANTIATE_POLYNOMIAL(TYPE) \ - template class polynomial<TYPE>; \ - template bool operator==(const polynomial<TYPE>& a, const polynomial<TYPE>& b); \ - template polynomial<TYPE> operator+(const polynomial<TYPE>& a, const polynomial<TYPE>& b); \ - template polynomial<TYPE> operator-(const polynomial<TYPE>& a, const polynomial<TYPE>& b); \ - template polynomial<TYPE> operator-(const polynomial<TYPE>& a); \ - template polynomial<TYPE> random_polynomial_bits(uint64_t n, const uint64_t bits); \ - template polynomial<TYPE> naive_product(const polynomial<TYPE>& a, const polynomial<TYPE>& b); \ - // template polynomial<TYPE> random_polynomial(uint64_t n); - -#endif // SPQLIOS_NEGACYCLIC_POLYNOMIAL_IMPL_H
D spqlios/lib/test/testlib/ntt120_dft.cpp

@@ -1,122 +0,0 @@

-#include "ntt120_dft.h" - -#include "mod_q120.h" - -// @brief alternative version of the NTT - -/** for all s=k/2^17, root_of_unity(s) = omega_0^k */ -static mod_q120 root_of_unity(double s) { - static mod_q120 omega_2pow17{OMEGA1, OMEGA2, OMEGA3, OMEGA4}; - static double _2pow17 = 1 << 17; - return pow(omega_2pow17, s * _2pow17); -} -static mod_q120 root_of_unity_inv(double s) { - static mod_q120 omega_2pow17{OMEGA1, OMEGA2, OMEGA3, OMEGA4}; - static double _2pow17 = 1 << 17; - return pow(omega_2pow17, -s * _2pow17); -} - -/** recursive naive ntt */ -static void q120_ntt_naive_rec(uint64_t n, double entry_pwr, mod_q120* data) { - if (n == 1) return; - const uint64_t h = n / 2; - const double s = entry_pwr / 2.; - mod_q120 om = root_of_unity(s); - for (uint64_t j = 0; j < h; ++j) { - mod_q120 om_right = data[h + j] * om; - data[h + j] = data[j] - om_right; - data[j] = data[j] + om_right; - } - q120_ntt_naive_rec(h, s, data); - q120_ntt_naive_rec(h, s + 0.5, data + h); -} -static void q120_intt_naive_rec(uint64_t n, double entry_pwr, mod_q120* data) { - if (n == 1) return; - const uint64_t h = n / 2; - const double s = entry_pwr / 2.; - q120_intt_naive_rec(h, s, data); - q120_intt_naive_rec(h, s + 0.5, data + h); - mod_q120 om = root_of_unity_inv(s); - for (uint64_t j = 0; j < h; ++j) { - mod_q120 dat_diff = half(data[j] - data[h + j]); - data[j] = half(data[j] + data[h + j]); - data[h + j] = dat_diff * om; - } -} - -/** user friendly version */ -q120_nttvec simple_ntt120(const znx_i64& polynomial) { - const uint64_t n = polynomial.nn(); - q120_nttvec res(n); - for (uint64_t i = 0; i < n; ++i) { - int64_t xi = polynomial.get_coeff(i); - res.v[i] = mod_q120(xi, xi, xi, xi); - } - q120_ntt_naive_rec(n, 0.5, res.v.data()); - return res; -} - -znx_i128 simple_intt120(const q120_nttvec& fftvec) { - const uint64_t n = fftvec.nn(); - q120_nttvec copy = fftvec; - znx_i128 res(n); - q120_intt_naive_rec(n, 0.5, copy.v.data()); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, copy.v[i].to_int128()); - } - return res; -} -bool operator==(const q120_nttvec& a, const q120_nttvec& b) { return a.v == b.v; } - -std::vector<mod_q120> q120_ntt_naive(const std::vector<mod_q120>& x) { - std::vector<mod_q120> res = x; - q120_ntt_naive_rec(res.size(), 0.5, res.data()); - return res; -} -q120_nttvec::q120_nttvec(uint64_t n) : v(n) {} -q120_nttvec::q120_nttvec(uint64_t n, const q120b* data) : v(n) { - int64_t* d = (int64_t*)data; - for (uint64_t i = 0; i < n; ++i) { - v[i] = mod_q120::from_q120b(d + 4 * i); - } -} -q120_nttvec::q120_nttvec(uint64_t n, const q120c* data) : v(n) { - int64_t* d = (int64_t*)data; - for (uint64_t i = 0; i < n; ++i) { - v[i] = mod_q120::from_q120c(d + 4 * i); - } -} -uint64_t q120_nttvec::nn() const { return v.size(); } -q120_nttvec q120_nttvec::zero(uint64_t n) { return q120_nttvec(n); } -void q120_nttvec::save_as(q120a* dest) const { - int64_t* const d = (int64_t*)dest; - const uint64_t n = nn(); - for (uint64_t i = 0; i < n; ++i) { - v[i].save_as_q120a(d + 4 * i); - } -} -void q120_nttvec::save_as(q120b* dest) const { - int64_t* const d = (int64_t*)dest; - const uint64_t n = nn(); - for (uint64_t i = 0; i < n; ++i) { - v[i].save_as_q120b(d + 4 * i); - } -} -void q120_nttvec::save_as(q120c* dest) const { - int64_t* const d = (int64_t*)dest; - const uint64_t n = nn(); - for (uint64_t i = 0; i < n; ++i) { - v[i].save_as_q120c(d + 4 * i); - } -} -mod_q120 q120_nttvec::get_blk(uint64_t blk) const { - REQUIRE_DRAMATICALLY(blk < nn(), "blk overflow"); - return v[blk]; -} -q120_nttvec q120_nttvec::random(uint64_t n) { - q120_nttvec res(n); - for (uint64_t i = 0; i < n; ++i) { - res.v[i] = uniform_q120(); - } - return res; -}
D spqlios/lib/test/testlib/ntt120_dft.h

@@ -1,31 +0,0 @@

-#ifndef SPQLIOS_NTT120_DFT_H -#define SPQLIOS_NTT120_DFT_H - -#include <vector> - -#include "../../spqlios/q120/q120_arithmetic.h" -#include "mod_q120.h" -#include "negacyclic_polynomial.h" -#include "test_commons.h" - -class q120_nttvec { - public: - std::vector<mod_q120> v; - q120_nttvec() = default; - explicit q120_nttvec(uint64_t n); - q120_nttvec(uint64_t n, const q120b* data); - q120_nttvec(uint64_t n, const q120c* data); - uint64_t nn() const; - static q120_nttvec zero(uint64_t n); - static q120_nttvec random(uint64_t n); - void save_as(q120a* dest) const; - void save_as(q120b* dest) const; - void save_as(q120c* dest) const; - mod_q120 get_blk(uint64_t blk) const; -}; - -q120_nttvec simple_ntt120(const znx_i64& polynomial); -znx_i128 simple_intt120(const q120_nttvec& fftvec); -bool operator==(const q120_nttvec& a, const q120_nttvec& b); - -#endif // SPQLIOS_NTT120_DFT_H
D spqlios/lib/test/testlib/ntt120_layouts.cpp

@@ -1,66 +0,0 @@

-#include "ntt120_layouts.h" - -mod_q120x2::mod_q120x2() {} -mod_q120x2::mod_q120x2(const mod_q120& a, const mod_q120& b) { - value[0] = a; - value[1] = b; -} -mod_q120x2::mod_q120x2(q120x2b* addr) { - uint64_t* p = (uint64_t*)addr; - value[0] = mod_q120::from_q120b(p); - value[1] = mod_q120::from_q120b(p + 4); -} - -ntt120_vec_znx_dft_layout::ntt120_vec_znx_dft_layout(uint64_t n, uint64_t size) - : nn(n), // - size(size), // - data((VEC_ZNX_DFT*)alloc64(n * size * 4 * sizeof(uint64_t))) {} - -mod_q120x2 ntt120_vec_znx_dft_layout::get_copy_zext(uint64_t idx, uint64_t blk) { - return mod_q120x2(get_blk(idx, blk)); -} -q120x2b* ntt120_vec_znx_dft_layout::get_blk(uint64_t idx, uint64_t blk) { - REQUIRE_DRAMATICALLY(idx < size, "idx overflow"); - REQUIRE_DRAMATICALLY(blk < nn / 2, "blk overflow"); - uint64_t* d = (uint64_t*)data; - return (q120x2b*)(d + 4 * nn * idx + 8 * blk); -} -ntt120_vec_znx_dft_layout::~ntt120_vec_znx_dft_layout() { spqlios_free(data); } -q120_nttvec ntt120_vec_znx_dft_layout::get_copy_zext(uint64_t idx) { - int64_t* d = (int64_t*)data; - if (idx < size) { - return q120_nttvec(nn, (q120b*)(d + idx * nn * 4)); - } else { - return q120_nttvec::zero(nn); - } -} -void ntt120_vec_znx_dft_layout::set(uint64_t idx, const q120_nttvec& value) { - REQUIRE_DRAMATICALLY(idx < size, "index overflow: " << idx << " / " << size); - q120b* dest_addr = (q120b*)((int64_t*)data + idx * nn * 4); - value.save_as(dest_addr); -} -void ntt120_vec_znx_dft_layout::fill_random() { - for (uint64_t i = 0; i < size; ++i) { - set(i, q120_nttvec::random(nn)); - } -} -thash ntt120_vec_znx_dft_layout::content_hash() const { return test_hash(data, nn * size * 4 * sizeof(int64_t)); } -ntt120_vec_znx_big_layout::ntt120_vec_znx_big_layout(uint64_t n, uint64_t size) - : nn(n), // - size(size), - data((VEC_ZNX_BIG*)alloc64(n * size * sizeof(__int128_t))) {} - -znx_i128 ntt120_vec_znx_big_layout::get_copy(uint64_t index) const { return znx_i128(nn, get_addr(index)); } -znx_i128 ntt120_vec_znx_big_layout::get_copy_zext(uint64_t index) const { - if (index < size) { - return znx_i128(nn, get_addr(index)); - } else { - return znx_i128::zero(nn); - } -} -__int128* ntt120_vec_znx_big_layout::get_addr(uint64_t index) const { - REQUIRE_DRAMATICALLY(index < size, "index overflow: " << index << " / " << size); - return (__int128_t*)data + index * nn; -} -void ntt120_vec_znx_big_layout::set(uint64_t index, const znx_i128& value) { value.save_as(get_addr(index)); } -ntt120_vec_znx_big_layout::~ntt120_vec_znx_big_layout() { spqlios_free(data); }
D spqlios/lib/test/testlib/ntt120_layouts.h

@@ -1,103 +0,0 @@

-#ifndef SPQLIOS_NTT120_LAYOUTS_H -#define SPQLIOS_NTT120_LAYOUTS_H - -#include "../../spqlios/arithmetic/vec_znx_arithmetic.h" -#include "mod_q120.h" -#include "negacyclic_polynomial.h" -#include "ntt120_dft.h" -#include "test_commons.h" - -struct q120b_vector_view {}; - -struct mod_q120x2 { - mod_q120 value[2]; - mod_q120x2(); - mod_q120x2(const mod_q120& a, const mod_q120& b); - mod_q120x2(__int128_t value); - explicit mod_q120x2(q120x2b* addr); - explicit mod_q120x2(q120x2c* addr); - void save_as(q120x2b* addr) const; - void save_as(q120x2c* addr) const; - static mod_q120x2 random(); -}; -mod_q120x2 operator+(const mod_q120x2& a, const mod_q120x2& b); -mod_q120x2 operator-(const mod_q120x2& a, const mod_q120x2& b); -mod_q120x2 operator*(const mod_q120x2& a, const mod_q120x2& b); -bool operator==(const mod_q120x2& a, const mod_q120x2& b); -bool operator!=(const mod_q120x2& a, const mod_q120x2& b); -mod_q120x2& operator+=(mod_q120x2& a, const mod_q120x2& b); -mod_q120x2& operator-=(mod_q120x2& a, const mod_q120x2& b); - -/** @brief test layout for the VEC_ZNX_DFT */ -struct ntt120_vec_znx_dft_layout { - const uint64_t nn; - const uint64_t size; - VEC_ZNX_DFT* const data; - ntt120_vec_znx_dft_layout(uint64_t n, uint64_t size); - mod_q120x2 get_copy_zext(uint64_t idx, uint64_t blk); - q120_nttvec get_copy_zext(uint64_t idx); - void set(uint64_t idx, const q120_nttvec& v); - q120x2b* get_blk(uint64_t idx, uint64_t blk); - thash content_hash() const; - void fill_random(); - ~ntt120_vec_znx_dft_layout(); -}; - -/** @brief test layout for the VEC_ZNX_BIG */ -class ntt120_vec_znx_big_layout { - public: - const uint64_t nn; - const uint64_t size; - VEC_ZNX_BIG* const data; - ntt120_vec_znx_big_layout(uint64_t n, uint64_t size); - - private: - __int128* get_addr(uint64_t index) const; - - public: - znx_i128 get_copy(uint64_t index) const; - znx_i128 get_copy_zext(uint64_t index) const; - void set(uint64_t index, const znx_i128& value); - ~ntt120_vec_znx_big_layout(); -}; - -/** @brief test layout for the VMP_PMAT */ -class ntt120_vmp_pmat_layout { - const uint64_t nn; - const uint64_t nrows; - const uint64_t ncols; - VMP_PMAT* const data; - ntt120_vmp_pmat_layout(uint64_t n, uint64_t nrows, uint64_t ncols); - mod_q120x2 get(uint64_t row, uint64_t col, uint64_t blk) const; - ~ntt120_vmp_pmat_layout(); -}; - -/** @brief test layout for the SVP_PPOL */ -class ntt120_svp_ppol_layout { - const uint64_t nn; - SVP_PPOL* const data; - ntt120_svp_ppol_layout(uint64_t n); - ~ntt120_svp_ppol_layout(); -}; - -/** @brief test layout for the CNV_PVEC_L */ -class ntt120_cnv_left_layout { - const uint64_t nn; - const uint64_t size; - CNV_PVEC_L* const data; - ntt120_cnv_left_layout(uint64_t n, uint64_t size); - mod_q120x2 get(uint64_t idx, uint64_t blk); - ~ntt120_cnv_left_layout(); -}; - -/** @brief test layout for the CNV_PVEC_R */ -class ntt120_cnv_right_layout { - const uint64_t nn; - const uint64_t size; - CNV_PVEC_R* const data; - ntt120_cnv_right_layout(uint64_t n, uint64_t size); - mod_q120x2 get(uint64_t idx, uint64_t blk); - ~ntt120_cnv_right_layout(); -}; - -#endif // SPQLIOS_NTT120_LAYOUTS_H
D spqlios/lib/test/testlib/polynomial_vector.cpp

@@ -1,69 +0,0 @@

-#include "polynomial_vector.h" - -#include <cstring> - -#ifdef VALGRIND_MEM_TESTS -#include "valgrind/memcheck.h" -#endif - -#define CANARY_PADDING (1024) -#define GARBAGE_VALUE (242) - -znx_vec_i64_layout::znx_vec_i64_layout(uint64_t n, uint64_t size, uint64_t slice) : n(n), size(size), slice(slice) { - REQUIRE_DRAMATICALLY(is_pow2(n), "not a power of 2" << n); - REQUIRE_DRAMATICALLY(slice >= n, "slice too small" << slice << " < " << n); - this->region = (uint8_t*)malloc(size * slice * sizeof(int64_t) + 2 * CANARY_PADDING); - this->data_start = (int64_t*)(region + CANARY_PADDING); - // ensure that any invalid value is kind-of garbage - memset(region, GARBAGE_VALUE, size * slice * sizeof(int64_t) + 2 * CANARY_PADDING); - // mark inter-slice memory as non accessible -#ifdef VALGRIND_MEM_TESTS - VALGRIND_MAKE_MEM_NOACCESS(region, CANARY_PADDING); - VALGRIND_MAKE_MEM_NOACCESS(region + size * slice * sizeof(int64_t) + CANARY_PADDING, CANARY_PADDING); - for (uint64_t i = 0; i < size; ++i) { - VALGRIND_MAKE_MEM_UNDEFINED(data_start + i * slice, n * sizeof(int64_t)); - } - if (size != slice) { - for (uint64_t i = 0; i < size; ++i) { - VALGRIND_MAKE_MEM_NOACCESS(data_start + i * slice + n, (slice - n) * sizeof(int64_t)); - } - } -#endif -} - -znx_vec_i64_layout::~znx_vec_i64_layout() { free(region); } - -znx_i64 znx_vec_i64_layout::get_copy_zext(uint64_t index) const { - if (index < size) { - return znx_i64(n, data_start + index * slice); - } else { - return znx_i64::zero(n); - } -} - -znx_i64 znx_vec_i64_layout::get_copy(uint64_t index) const { - REQUIRE_DRAMATICALLY(index < size, "index overflow: " << index << " / " << size); - return znx_i64(n, data_start + index * slice); -} - -void znx_vec_i64_layout::set(uint64_t index, const znx_i64& elem) { - REQUIRE_DRAMATICALLY(index < size, "index overflow: " << index << " / " << size); - REQUIRE_DRAMATICALLY(elem.nn() == n, "incompatible ring dimensions: " << elem.nn() << " / " << n); - elem.save_as(data_start + index * slice); -} - -int64_t* znx_vec_i64_layout::data() { return data_start; } -const int64_t* znx_vec_i64_layout::data() const { return data_start; } - -void znx_vec_i64_layout::fill_random(uint64_t bits) { - for (uint64_t i = 0; i < size; ++i) { - set(i, znx_i64::random_log2bound(n, bits)); - } -} -__uint128_t znx_vec_i64_layout::content_hash() const { - test_hasher hasher; - for (uint64_t i = 0; i < size; ++i) { - hasher.update(data() + i * slice, n * sizeof(int64_t)); - } - return hasher.hash(); -}
D spqlios/lib/test/testlib/polynomial_vector.h

@@ -1,42 +0,0 @@

-#ifndef SPQLIOS_POLYNOMIAL_VECTOR_H -#define SPQLIOS_POLYNOMIAL_VECTOR_H - -#include "negacyclic_polynomial.h" -#include "test_commons.h" - -/** @brief a test memory layout for znx i64 polynomials vectors */ -class znx_vec_i64_layout { - uint64_t n; - uint64_t size; - uint64_t slice; - int64_t* data_start; - uint8_t* region; - - public: - // NO-COPY structure - znx_vec_i64_layout(const znx_vec_i64_layout&) = delete; - void operator=(const znx_vec_i64_layout&) = delete; - znx_vec_i64_layout(znx_vec_i64_layout&&) = delete; - void operator=(znx_vec_i64_layout&&) = delete; - /** @brief initialises a memory layout */ - znx_vec_i64_layout(uint64_t n, uint64_t size, uint64_t slice); - /** @brief destructor */ - ~znx_vec_i64_layout(); - - /** @brief get a copy of item index index (extended with zeros) */ - znx_i64 get_copy_zext(uint64_t index) const; - /** @brief get a copy of item index index (extended with zeros) */ - znx_i64 get_copy(uint64_t index) const; - /** @brief get a copy of item index index (index<size) */ - void set(uint64_t index, const znx_i64& elem); - /** @brief fill with random values */ - void fill_random(uint64_t bits = 63); - /** @brief raw pointer access */ - int64_t* data(); - /** @brief raw pointer access (const version) */ - const int64_t* data() const; - /** @brief content hashcode */ - __uint128_t content_hash() const; -}; - -#endif // SPQLIOS_POLYNOMIAL_VECTOR_H
D spqlios/lib/test/testlib/random.cpp

@@ -1,55 +0,0 @@

-#include <cstdint> -#include <random> - -#include "test_commons.h" - -bool is_pow2(uint64_t n) { return !(n & (n - 1)); } - -test_rng& randgen() { - static test_rng gen; - return gen; -} -uint64_t uniform_u64() { - static std::uniform_int_distribution<uint64_t> dist64(0, UINT64_MAX); - return dist64(randgen()); -} - -uint64_t uniform_u64_bits(uint64_t nbits) { - if (nbits >= 64) return uniform_u64(); - return uniform_u64() >> (64 - nbits); -} - -int64_t uniform_i64() { - std::uniform_int_distribution<int64_t> dist; - return dist(randgen()); -} - -int64_t uniform_i64_bits(uint64_t nbits) { - int64_t bound = int64_t(1) << nbits; - std::uniform_int_distribution<int64_t> dist(-bound, bound); - return dist(randgen()); -} - -int64_t uniform_i64_bounds(const int64_t lb, const int64_t ub) { - std::uniform_int_distribution<int64_t> dist(lb, ub); - return dist(randgen()); -} - -__int128_t uniform_i128_bounds(const __int128_t lb, const __int128_t ub) { - std::uniform_int_distribution<__int128_t> dist(lb, ub); - return dist(randgen()); -} - -double random_f64_gaussian(double stdev) { - std::normal_distribution<double> dist(0, stdev); - return dist(randgen()); -} - -double uniform_f64_bounds(const double lb, const double ub) { - std::uniform_real_distribution<double> dist(lb, ub); - return dist(randgen()); -} - -double uniform_f64_01() { - return uniform_f64_bounds(0, 1); -}
D spqlios/lib/test/testlib/reim4_elem.cpp

@@ -1,145 +0,0 @@

-#include "reim4_elem.h" - -reim4_elem::reim4_elem(const double* re, const double* im) { - for (uint64_t i = 0; i < 4; ++i) { - value[i] = re[i]; - value[4 + i] = im[i]; - } -} -reim4_elem::reim4_elem(const double* layout) { - for (uint64_t i = 0; i < 8; ++i) { - value[i] = layout[i]; - } -} -reim4_elem::reim4_elem() { - for (uint64_t i = 0; i < 8; ++i) { - value[i] = 0.; - } -} -void reim4_elem::save_re_im(double* re, double* im) const { - for (uint64_t i = 0; i < 4; ++i) { - re[i] = value[i]; - im[i] = value[4 + i]; - } -} -void reim4_elem::save_as(double* reim4) const { - for (uint64_t i = 0; i < 8; ++i) { - reim4[i] = value[i]; - } -} -reim4_elem reim4_elem::zero() { return reim4_elem(); } - -bool operator==(const reim4_elem& x, const reim4_elem& y) { - for (uint64_t i = 0; i < 8; ++i) { - if (x.value[i] != y.value[i]) return false; - } - return true; -} - -reim4_elem gaussian_reim4() { - test_rng& gen = randgen(); - std::normal_distribution<double> dist(0, 1); - reim4_elem res; - for (uint64_t i = 0; i < 8; ++i) { - res.value[i] = dist(gen); - } - return res; -} - -reim4_array_view::reim4_array_view(uint64_t size, double* data) : size(size), data(data) {} -reim4_elem reim4_array_view::get(uint64_t i) const { - REQUIRE_DRAMATICALLY(i < size, "reim4 array overflow"); - return reim4_elem(data + 8 * i); -} -void reim4_array_view::set(uint64_t i, const reim4_elem& value) { - REQUIRE_DRAMATICALLY(i < size, "reim4 array overflow"); - value.save_as(data + 8 * i); -} - -reim_view::reim_view(uint64_t m, double* data) : m(m), data(data) {} -reim4_elem reim_view::get_blk(uint64_t i) { - REQUIRE_DRAMATICALLY(i < m / 4, "block overflow"); - return reim4_elem(data + 4 * i, data + m + 4 * i); -} -void reim_view::set_blk(uint64_t i, const reim4_elem& value) { - REQUIRE_DRAMATICALLY(i < m / 4, "block overflow"); - value.save_re_im(data + 4 * i, data + m + 4 * i); -} - -reim_vector_view::reim_vector_view(uint64_t m, uint64_t nrows, double* data) : m(m), nrows(nrows), data(data) {} -reim_view reim_vector_view::row(uint64_t row) { - REQUIRE_DRAMATICALLY(row < nrows, "row overflow"); - return reim_view(m, data + 2 * m * row); -} - -/** @brief addition */ -reim4_elem operator+(const reim4_elem& x, const reim4_elem& y) { - reim4_elem reps; - for (uint64_t i = 0; i < 8; ++i) { - reps.value[i] = x.value[i] + y.value[i]; - } - return reps; -} -reim4_elem& operator+=(reim4_elem& x, const reim4_elem& y) { - for (uint64_t i = 0; i < 8; ++i) { - x.value[i] += y.value[i]; - } - return x; -} -/** @brief subtraction */ -reim4_elem operator-(const reim4_elem& x, const reim4_elem& y) { - reim4_elem reps; - for (uint64_t i = 0; i < 8; ++i) { - reps.value[i] = x.value[i] + y.value[i]; - } - return reps; -} -reim4_elem& operator-=(reim4_elem& x, const reim4_elem& y) { - for (uint64_t i = 0; i < 8; ++i) { - x.value[i] -= y.value[i]; - } - return x; -} -/** @brief product */ -reim4_elem operator*(const reim4_elem& x, const reim4_elem& y) { - reim4_elem reps; - for (uint64_t i = 0; i < 4; ++i) { - double xre = x.value[i]; - double yre = y.value[i]; - double xim = x.value[i + 4]; - double yim = y.value[i + 4]; - reps.value[i] = xre * yre - xim * yim; - reps.value[i + 4] = xre * yim + xim * yre; - } - return reps; -} -/** @brief distance in infty norm */ -double infty_dist(const reim4_elem& x, const reim4_elem& y) { - double dist = 0; - for (uint64_t i = 0; i < 8; ++i) { - double d = fabs(x.value[i] - y.value[i]); - if (d > dist) dist = d; - } - return dist; -} - -std::ostream& operator<<(std::ostream& out, const reim4_elem& x) { - out << "[\n"; - for (uint64_t i = 0; i < 4; ++i) { - out << " re=" << x.value[i] << ", im=" << x.value[i + 4] << "\n"; - } - return out << "]"; -} - -reim4_matrix_view::reim4_matrix_view(uint64_t nrows, uint64_t ncols, double* data) - : nrows(nrows), ncols(ncols), data(data) {} -reim4_elem reim4_matrix_view::get(uint64_t row, uint64_t col) const { - REQUIRE_DRAMATICALLY(row < nrows, "rows out of bounds" << row << " / " << nrows); - REQUIRE_DRAMATICALLY(col < ncols, "cols out of bounds" << col << " / " << ncols); - return reim4_elem(data + 8 * (row * ncols + col)); -} -void reim4_matrix_view::set(uint64_t row, uint64_t col, const reim4_elem& value) { - REQUIRE_DRAMATICALLY(row < nrows, "rows out of bounds" << row << " / " << nrows); - REQUIRE_DRAMATICALLY(col < ncols, "cols out of bounds" << col << " / " << ncols); - value.save_as(data + 8 * (row * ncols + col)); -}
D spqlios/lib/test/testlib/reim4_elem.h

@@ -1,95 +0,0 @@

-#ifndef SPQLIOS_REIM4_ELEM_H -#define SPQLIOS_REIM4_ELEM_H - -#include "test_commons.h" - -/** @brief test class representing one single reim4 element */ -class reim4_elem { - public: - /** @brief 8 components (4 real parts followed by 4 imag parts) */ - double value[8]; - /** @brief constructs from 4 real parts and 4 imaginary parts */ - reim4_elem(const double* re, const double* im); - /** @brief constructs from 8 components */ - explicit reim4_elem(const double* layout); - /** @brief zero */ - reim4_elem(); - /** @brief saves the real parts to re and the 4 imag to im */ - void save_re_im(double* re, double* im) const; - /** @brief saves the 8 components to reim4 */ - void save_as(double* reim4) const; - static reim4_elem zero(); -}; - -/** @brief checks for equality */ -bool operator==(const reim4_elem& x, const reim4_elem& y); -/** @brief random gaussian reim4 of stdev 1 and mean 0 */ -reim4_elem gaussian_reim4(); -/** @brief addition */ -reim4_elem operator+(const reim4_elem& x, const reim4_elem& y); -reim4_elem& operator+=(reim4_elem& x, const reim4_elem& y); -/** @brief subtraction */ -reim4_elem operator-(const reim4_elem& x, const reim4_elem& y); -reim4_elem& operator-=(reim4_elem& x, const reim4_elem& y); -/** @brief product */ -reim4_elem operator*(const reim4_elem& x, const reim4_elem& y); -std::ostream& operator<<(std::ostream& out, const reim4_elem& x); -/** @brief distance in infty norm */ -double infty_dist(const reim4_elem& x, const reim4_elem& y); - -/** @brief test class representing the view of one reim of m complexes */ -class reim4_array_view { - uint64_t size; ///< size of the reim array - double* data; ///< pointer to the start of the array - public: - /** @brief ininitializes a view at an existing given address */ - reim4_array_view(uint64_t size, double* data); - ; - /** @brief gets the i-th element */ - reim4_elem get(uint64_t i) const; - /** @brief sets the i-th element */ - void set(uint64_t i, const reim4_elem& value); -}; - -/** @brief test class representing the view of one matrix of nrowsxncols reim4's */ -class reim4_matrix_view { - uint64_t nrows; ///< number of rows - uint64_t ncols; ///< number of columns - double* data; ///< pointer to the start of the matrix - public: - /** @brief ininitializes a view at an existing given address */ - reim4_matrix_view(uint64_t nrows, uint64_t ncols, double* data); - /** @brief gets the i-th element */ - reim4_elem get(uint64_t row, uint64_t col) const; - /** @brief sets the i-th element */ - void set(uint64_t row, uint64_t col, const reim4_elem& value); -}; - -/** @brief test class representing the view of one reim of m complexes */ -class reim_view { - uint64_t m; ///< (complex) dimension of the reim polynomial - double* data; ///< address of the start of the reim polynomial - public: - /** @brief ininitializes a view at an existing given address */ - reim_view(uint64_t m, double* data); - ; - /** @brief extracts the i-th reim4 block (i<m/4) */ - reim4_elem get_blk(uint64_t i); - /** @brief sets the i-th reim4 block (i<m/4) */ - void set_blk(uint64_t i, const reim4_elem& value); -}; - -/** @brief view of one contiguous reim vector */ -class reim_vector_view { - uint64_t m; ///< (complex) dimension of the reim polynomial - uint64_t nrows; ///< number of reim polynomials - double* data; ///< address of the start of the reim polynomial - - public: - /** @brief ininitializes a view at an existing given address */ - reim_vector_view(uint64_t m, uint64_t nrows, double* data); - /** @brief view of the given reim */ - reim_view row(uint64_t row); -}; - -#endif // SPQLIOS_REIM4_ELEM_H
D spqlios/lib/test/testlib/sha3.c

@@ -1,168 +0,0 @@

-// sha3.c -// 19-Nov-11 Markku-Juhani O. Saarinen <mjos@iki.fi> -// https://github.com/mjosaarinen/tiny_sha3 -// LICENSE: MIT - -// Revised 07-Aug-15 to match with official release of FIPS PUB 202 "SHA3" -// Revised 03-Sep-15 for portability + OpenSSL - style API - -#include "sha3.h" - -// update the state with given number of rounds - -void sha3_keccakf(uint64_t st[25]) { - // constants - const uint64_t keccakf_rndc[24] = {0x0000000000000001, 0x0000000000008082, 0x800000000000808a, 0x8000000080008000, - 0x000000000000808b, 0x0000000080000001, 0x8000000080008081, 0x8000000000008009, - 0x000000000000008a, 0x0000000000000088, 0x0000000080008009, 0x000000008000000a, - 0x000000008000808b, 0x800000000000008b, 0x8000000000008089, 0x8000000000008003, - 0x8000000000008002, 0x8000000000000080, 0x000000000000800a, 0x800000008000000a, - 0x8000000080008081, 0x8000000000008080, 0x0000000080000001, 0x8000000080008008}; - const int keccakf_rotc[24] = {1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, - 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44}; - const int keccakf_piln[24] = {10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1}; - - // variables - int i, j, r; - uint64_t t, bc[5]; - -#if __BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__ - uint8_t* v; - - // endianess conversion. this is redundant on little-endian targets - for (i = 0; i < 25; i++) { - v = (uint8_t*)&st[i]; - st[i] = ((uint64_t)v[0]) | (((uint64_t)v[1]) << 8) | (((uint64_t)v[2]) << 16) | (((uint64_t)v[3]) << 24) | - (((uint64_t)v[4]) << 32) | (((uint64_t)v[5]) << 40) | (((uint64_t)v[6]) << 48) | (((uint64_t)v[7]) << 56); - } -#endif - - // actual iteration - for (r = 0; r < KECCAKF_ROUNDS; r++) { - // Theta - for (i = 0; i < 5; i++) bc[i] = st[i] ^ st[i + 5] ^ st[i + 10] ^ st[i + 15] ^ st[i + 20]; - - for (i = 0; i < 5; i++) { - t = bc[(i + 4) % 5] ^ ROTL64(bc[(i + 1) % 5], 1); - for (j = 0; j < 25; j += 5) st[j + i] ^= t; - } - - // Rho Pi - t = st[1]; - for (i = 0; i < 24; i++) { - j = keccakf_piln[i]; - bc[0] = st[j]; - st[j] = ROTL64(t, keccakf_rotc[i]); - t = bc[0]; - } - - // Chi - for (j = 0; j < 25; j += 5) { - for (i = 0; i < 5; i++) bc[i] = st[j + i]; - for (i = 0; i < 5; i++) st[j + i] ^= (~bc[(i + 1) % 5]) & bc[(i + 2) % 5]; - } - - // Iota - st[0] ^= keccakf_rndc[r]; - } - -#if __BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__ - // endianess conversion. this is redundant on little-endian targets - for (i = 0; i < 25; i++) { - v = (uint8_t*)&st[i]; - t = st[i]; - v[0] = t & 0xFF; - v[1] = (t >> 8) & 0xFF; - v[2] = (t >> 16) & 0xFF; - v[3] = (t >> 24) & 0xFF; - v[4] = (t >> 32) & 0xFF; - v[5] = (t >> 40) & 0xFF; - v[6] = (t >> 48) & 0xFF; - v[7] = (t >> 56) & 0xFF; - } -#endif -} - -// Initialize the context for SHA3 - -int sha3_init(sha3_ctx_t* c, int mdlen) { - int i; - - for (i = 0; i < 25; i++) c->st.q[i] = 0; - c->mdlen = mdlen; - c->rsiz = 200 - 2 * mdlen; - c->pt = 0; - - return 1; -} - -// update state with more data - -int sha3_update(sha3_ctx_t* c, const void* data, size_t len) { - size_t i; - int j; - - j = c->pt; - for (i = 0; i < len; i++) { - c->st.b[j++] ^= ((const uint8_t*)data)[i]; - if (j >= c->rsiz) { - sha3_keccakf(c->st.q); - j = 0; - } - } - c->pt = j; - - return 1; -} - -// finalize and output a hash - -int sha3_final(void* md, sha3_ctx_t* c) { - int i; - - c->st.b[c->pt] ^= 0x06; - c->st.b[c->rsiz - 1] ^= 0x80; - sha3_keccakf(c->st.q); - - for (i = 0; i < c->mdlen; i++) { - ((uint8_t*)md)[i] = c->st.b[i]; - } - - return 1; -} - -// compute a SHA-3 hash (md) of given byte length from "in" - -void* sha3(const void* in, size_t inlen, void* md, int mdlen) { - sha3_ctx_t sha3; - - sha3_init(&sha3, mdlen); - sha3_update(&sha3, in, inlen); - sha3_final(md, &sha3); - - return md; -} - -// SHAKE128 and SHAKE256 extensible-output functionality - -void shake_xof(sha3_ctx_t* c) { - c->st.b[c->pt] ^= 0x1F; - c->st.b[c->rsiz - 1] ^= 0x80; - sha3_keccakf(c->st.q); - c->pt = 0; -} - -void shake_out(sha3_ctx_t* c, void* out, size_t len) { - size_t i; - int j; - - j = c->pt; - for (i = 0; i < len; i++) { - if (j >= c->rsiz) { - sha3_keccakf(c->st.q); - j = 0; - } - ((uint8_t*)out)[i] = c->st.b[j++]; - } - c->pt = j; -}
D spqlios/lib/test/testlib/sha3.h

@@ -1,56 +0,0 @@

-// sha3.h -// 19-Nov-11 Markku-Juhani O. Saarinen <mjos@iki.fi> -// https://github.com/mjosaarinen/tiny_sha3 -// License: MIT - -#ifndef SHA3_H -#define SHA3_H - -#ifdef __cplusplus -extern "C" { -#endif - -#include <stddef.h> -#include <stdint.h> - -#ifndef KECCAKF_ROUNDS -#define KECCAKF_ROUNDS 24 -#endif - -#ifndef ROTL64 -#define ROTL64(x, y) (((x) << (y)) | ((x) >> (64 - (y)))) -#endif - -// state context -typedef struct { - union { // state: - uint8_t b[200]; // 8-bit bytes - uint64_t q[25]; // 64-bit words - } st; - int pt, rsiz, mdlen; // these don't overflow -} sha3_ctx_t; - -// Compression function. -void sha3_keccakf(uint64_t st[25]); - -// OpenSSL - like interfece -int sha3_init(sha3_ctx_t* c, int mdlen); // mdlen = hash output in bytes -int sha3_update(sha3_ctx_t* c, const void* data, size_t len); -int sha3_final(void* md, sha3_ctx_t* c); // digest goes to md - -// compute a sha3 hash (md) of given byte length from "in" -void* sha3(const void* in, size_t inlen, void* md, int mdlen); - -// SHAKE128 and SHAKE256 extensible-output functions -#define shake128_init(c) sha3_init(c, 16) -#define shake256_init(c) sha3_init(c, 32) -#define shake_update sha3_update - -void shake_xof(sha3_ctx_t* c); -void shake_out(sha3_ctx_t* c, void* out, size_t len); - -#ifdef __cplusplus -} -#endif - -#endif // SHA3_H
D spqlios/lib/test/testlib/test_commons.cpp

@@ -1,10 +0,0 @@

-#include "test_commons.h" - -#include <inttypes.h> - -std::ostream& operator<<(std::ostream& out, __int128_t x) { - char c[35] = {0}; - snprintf(c, 35, "0x%016" PRIx64 "%016" PRIx64, uint64_t(x >> 64), uint64_t(x)); - return out << c; -} -std::ostream& operator<<(std::ostream& out, __uint128_t x) { return out << __int128_t(x); }
D spqlios/lib/test/testlib/test_commons.h

@@ -1,74 +0,0 @@

-#ifndef SPQLIOS_TEST_COMMONS_H -#define SPQLIOS_TEST_COMMONS_H - -#include <iostream> -#include <random> - -#include "../../spqlios/commons.h" - -/** @brief macro that crashes if the condition are not met */ -#define REQUIRE_DRAMATICALLY(req_contition, error_msg) \ - do { \ - if (!(req_contition)) { \ - std::cerr << "REQUIREMENT FAILED at " << __FILE__ << ":" << __LINE__ << ": " << error_msg << std::endl; \ - abort(); \ - } \ - } while (0) - -typedef std::default_random_engine test_rng; -/** @brief reference to the default test rng */ -test_rng& randgen(); -/** @brief uniformly random 64-bit uint */ -uint64_t uniform_u64(); -/** @brief uniformly random number <= 2^nbits-1 */ -uint64_t uniform_u64_bits(uint64_t nbits); -/** @brief uniformly random signed 64-bit number */ -int64_t uniform_i64(); -/** @brief uniformly random signed |number| <= 2^nbits */ -int64_t uniform_i64_bits(uint64_t nbits); -/** @brief uniformly random signed lb <= number <= ub */ -int64_t uniform_i64_bounds(const int64_t lb, const int64_t ub); -/** @brief uniformly random signed lb <= number <= ub */ -__int128_t uniform_i128_bounds(const __int128_t lb, const __int128_t ub); -/** @brief uniformly random gaussian float64 */ -double random_f64_gaussian(double stdev = 1); -/** @brief uniformly random signed lb <= number <= ub */ -double uniform_f64_bounds(const double lb, const double ub); -/** @brief uniformly random float64 in [0,1] */ -double uniform_f64_01(); -/** @brief random gaussian float64 */ -double random_f64_gaussian(double stdev); - -bool is_pow2(uint64_t n); - -void* alloc64(uint64_t size); - -typedef __uint128_t thash; -/** @brief returns some pseudorandom hash of a contiguous content */ -thash test_hash(const void* data, uint64_t size); -/** @brief class to return a pseudorandom hash of a piecewise-defined content */ -class test_hasher { - void* md; - public: - test_hasher(); - test_hasher(const test_hasher&) = delete; - void operator=(const test_hasher&) = delete; - /** - * @brief append input bytes. - * The final hash only depends on the concatenation of bytes, not on the - * way the content was split into multiple calls to update. - */ - void update(const void* data, uint64_t size); - /** - * @brief returns the final hash. - * no more calls to update(...) shall be issued after this call. - */ - thash hash(); - ~test_hasher(); -}; - -// not included by default, since it makes some versions of gtest not compile -// std::ostream& operator<<(std::ostream& out, __int128_t x); -// std::ostream& operator<<(std::ostream& out, __uint128_t x); - -#endif // SPQLIOS_TEST_COMMONS_H
D spqlios/lib/test/testlib/test_hash.cpp

@@ -1,24 +0,0 @@

-#include "sha3.h" -#include "test_commons.h" - -/** @brief returns some pseudorandom hash of the content */ -thash test_hash(const void* data, uint64_t size) { - thash res; - sha3(data, size, &res, sizeof(res)); - return res; -} -/** @brief class to return a pseudorandom hash of the content */ -test_hasher::test_hasher() { - md = malloc(sizeof(sha3_ctx_t)); - sha3_init((sha3_ctx_t*)md, 16); -} - -void test_hasher::update(const void* data, uint64_t size) { sha3_update((sha3_ctx_t*)md, data, size); } - -thash test_hasher::hash() { - thash res; - sha3_final(&res, (sha3_ctx_t*)md); - return res; -} - -test_hasher::~test_hasher() { free(md); }
D spqlios/lib/test/testlib/vec_rnx_layout.cpp

@@ -1,182 +0,0 @@

-#include "vec_rnx_layout.h" - -#include <cstring> - -#include "../../spqlios/arithmetic/vec_rnx_arithmetic.h" - -#ifdef VALGRIND_MEM_TESTS -#include "valgrind/memcheck.h" -#endif - -#define CANARY_PADDING (1024) -#define GARBAGE_VALUE (242) - -rnx_vec_f64_layout::rnx_vec_f64_layout(uint64_t n, uint64_t size, uint64_t slice) : n(n), size(size), slice(slice) { - REQUIRE_DRAMATICALLY(is_pow2(n), "not a power of 2" << n); - REQUIRE_DRAMATICALLY(slice >= n, "slice too small" << slice << " < " << n); - this->region = (uint8_t*)malloc(size * slice * sizeof(int64_t) + 2 * CANARY_PADDING); - this->data_start = (double*)(region + CANARY_PADDING); - // ensure that any invalid value is kind-of garbage - memset(region, GARBAGE_VALUE, size * slice * sizeof(int64_t) + 2 * CANARY_PADDING); - // mark inter-slice memory as not accessible -#ifdef VALGRIND_MEM_TESTS - VALGRIND_MAKE_MEM_NOACCESS(region, CANARY_PADDING); - VALGRIND_MAKE_MEM_NOACCESS(region + size * slice * sizeof(int64_t) + CANARY_PADDING, CANARY_PADDING); - for (uint64_t i = 0; i < size; ++i) { - VALGRIND_MAKE_MEM_UNDEFINED(data_start + i * slice, n * sizeof(int64_t)); - } - if (size != slice) { - for (uint64_t i = 0; i < size; ++i) { - VALGRIND_MAKE_MEM_NOACCESS(data_start + i * slice + n, (slice - n) * sizeof(int64_t)); - } - } -#endif -} - -rnx_vec_f64_layout::~rnx_vec_f64_layout() { free(region); } - -rnx_f64 rnx_vec_f64_layout::get_copy_zext(uint64_t index) const { - if (index < size) { - return rnx_f64(n, data_start + index * slice); - } else { - return rnx_f64::zero(n); - } -} - -rnx_f64 rnx_vec_f64_layout::get_copy(uint64_t index) const { - REQUIRE_DRAMATICALLY(index < size, "index overflow: " << index << " / " << size); - return rnx_f64(n, data_start + index * slice); -} - -reim_fft64vec rnx_vec_f64_layout::get_dft_copy_zext(uint64_t index) const { - if (index < size) { - return reim_fft64vec(n, data_start + index * slice); - } else { - return reim_fft64vec::zero(n); - } -} - -reim_fft64vec rnx_vec_f64_layout::get_dft_copy(uint64_t index) const { - REQUIRE_DRAMATICALLY(index < size, "index overflow: " << index << " / " << size); - return reim_fft64vec(n, data_start + index * slice); -} - -void rnx_vec_f64_layout::set(uint64_t index, const rnx_f64& elem) { - REQUIRE_DRAMATICALLY(index < size, "index overflow: " << index << " / " << size); - REQUIRE_DRAMATICALLY(elem.nn() == n, "incompatible ring dimensions: " << elem.nn() << " / " << n); - elem.save_as(data_start + index * slice); -} - -double* rnx_vec_f64_layout::data() { return data_start; } -const double* rnx_vec_f64_layout::data() const { return data_start; } - -void rnx_vec_f64_layout::fill_random(double log2bound) { - for (uint64_t i = 0; i < size; ++i) { - set(i, rnx_f64::random_log2bound(n, log2bound)); - } -} - -thash rnx_vec_f64_layout::content_hash() const { - test_hasher hasher; - for (uint64_t i = 0; i < size; ++i) { - hasher.update(data() + i * slice, n * sizeof(int64_t)); - } - return hasher.hash(); -} - -fft64_rnx_vmp_pmat_layout::fft64_rnx_vmp_pmat_layout(uint64_t n, uint64_t nrows, uint64_t ncols) - : nn(n), - nrows(nrows), - ncols(ncols), // - data((RNX_VMP_PMAT*)alloc64(nrows * ncols * nn * 8)) {} - -double* fft64_rnx_vmp_pmat_layout::get_addr(uint64_t row, uint64_t col, uint64_t blk) const { - REQUIRE_DRAMATICALLY(row < nrows, "row overflow: " << row << " / " << nrows); - REQUIRE_DRAMATICALLY(col < ncols, "col overflow: " << col << " / " << ncols); - REQUIRE_DRAMATICALLY(blk < nn / 8, "block overflow: " << blk << " / " << (nn / 8)); - double* d = (double*)data; - if (col == (ncols - 1) && (ncols % 2 == 1)) { - // special case: last column out of an odd column number - return d + blk * nrows * ncols * 8 // major: blk - + col * nrows * 8 // col == ncols-1 - + row * 8; - } else { - // general case: columns go by pair - return d + blk * nrows * ncols * 8 // major: blk - + (col / 2) * (2 * nrows) * 8 // second: col pair index - + row * 2 * 8 // third: row index - + (col % 2) * 8; // minor: col in colpair - } -} - -reim4_elem fft64_rnx_vmp_pmat_layout::get(uint64_t row, uint64_t col, uint64_t blk) const { - return reim4_elem(get_addr(row, col, blk)); -} -reim4_elem fft64_rnx_vmp_pmat_layout::get_zext(uint64_t row, uint64_t col, uint64_t blk) const { - REQUIRE_DRAMATICALLY(blk < nn / 8, "block overflow: " << blk << " / " << (nn / 8)); - if (row < nrows && col < ncols) { - return reim4_elem(get_addr(row, col, blk)); - } else { - return reim4_elem::zero(); - } -} -void fft64_rnx_vmp_pmat_layout::set(uint64_t row, uint64_t col, uint64_t blk, const reim4_elem& value) const { - value.save_as(get_addr(row, col, blk)); -} - -fft64_rnx_vmp_pmat_layout::~fft64_rnx_vmp_pmat_layout() { spqlios_free(data); } - -reim_fft64vec fft64_rnx_vmp_pmat_layout::get_zext(uint64_t row, uint64_t col) const { - if (row >= nrows || col >= ncols) { - return reim_fft64vec::zero(nn); - } - if (nn < 8) { - // the pmat is just col major - double* addr = (double*)data + (row + col * nrows) * nn; - return reim_fft64vec(nn, addr); - } - // otherwise, reconstruct it block by block - reim_fft64vec res(nn); - for (uint64_t blk = 0; blk < nn / 8; ++blk) { - reim4_elem v = get(row, col, blk); - res.set_blk(blk, v); - } - return res; -} -void fft64_rnx_vmp_pmat_layout::set(uint64_t row, uint64_t col, const reim_fft64vec& value) { - REQUIRE_DRAMATICALLY(row < nrows, "row overflow: " << row << " / " << nrows); - REQUIRE_DRAMATICALLY(col < ncols, "row overflow: " << col << " / " << ncols); - if (nn < 8) { - // the pmat is just col major - double* addr = (double*)data + (row + col * nrows) * nn; - value.save_as(addr); - return; - } - // otherwise, reconstruct it block by block - for (uint64_t blk = 0; blk < nn / 8; ++blk) { - reim4_elem v = value.get_blk(blk); - set(row, col, blk, v); - } -} -void fft64_rnx_vmp_pmat_layout::fill_random(double log2bound) { - for (uint64_t row = 0; row < nrows; ++row) { - for (uint64_t col = 0; col < ncols; ++col) { - set(row, col, reim_fft64vec::random(nn, log2bound)); - } - } -} - -fft64_rnx_svp_ppol_layout::fft64_rnx_svp_ppol_layout(uint64_t n) - : nn(n), // - data((RNX_SVP_PPOL*)alloc64(nn * 8)) {} - -reim_fft64vec fft64_rnx_svp_ppol_layout::get_copy() const { return reim_fft64vec(nn, (double*)data); } - -void fft64_rnx_svp_ppol_layout::set(const reim_fft64vec& value) { value.save_as((double*)data); } - -void fft64_rnx_svp_ppol_layout::fill_dft_random(uint64_t log2bound) { set(reim_fft64vec::dft_random(nn, log2bound)); } - -void fft64_rnx_svp_ppol_layout::fill_random(double log2bound) { set(reim_fft64vec::random(nn, log2bound)); } - -fft64_rnx_svp_ppol_layout::~fft64_rnx_svp_ppol_layout() { spqlios_free(data); } -thash fft64_rnx_svp_ppol_layout::content_hash() const { return test_hash(data, nn * sizeof(double)); }
D spqlios/lib/test/testlib/vec_rnx_layout.h

@@ -1,85 +0,0 @@

-#ifndef SPQLIOS_EXT_VEC_RNX_LAYOUT_H -#define SPQLIOS_EXT_VEC_RNX_LAYOUT_H - -#include "../../spqlios/arithmetic/vec_rnx_arithmetic.h" -#include "fft64_dft.h" -#include "negacyclic_polynomial.h" -#include "reim4_elem.h" -#include "test_commons.h" - -/** @brief a test memory layout for rnx i64 polynomials vectors */ -class rnx_vec_f64_layout { - uint64_t n; - uint64_t size; - uint64_t slice; - double* data_start; - uint8_t* region; - - public: - // NO-COPY structure - rnx_vec_f64_layout(const rnx_vec_f64_layout&) = delete; - void operator=(const rnx_vec_f64_layout&) = delete; - rnx_vec_f64_layout(rnx_vec_f64_layout&&) = delete; - void operator=(rnx_vec_f64_layout&&) = delete; - /** @brief initialises a memory layout */ - rnx_vec_f64_layout(uint64_t n, uint64_t size, uint64_t slice); - /** @brief destructor */ - ~rnx_vec_f64_layout(); - - /** @brief get a copy of item index index (extended with zeros) */ - rnx_f64 get_copy_zext(uint64_t index) const; - /** @brief get a copy of item index index (extended with zeros) */ - rnx_f64 get_copy(uint64_t index) const; - /** @brief get a copy of item index index (extended with zeros) */ - reim_fft64vec get_dft_copy_zext(uint64_t index) const; - /** @brief get a copy of item index index (extended with zeros) */ - reim_fft64vec get_dft_copy(uint64_t index) const; - - /** @brief get a copy of item index index (index<size) */ - void set(uint64_t index, const rnx_f64& elem); - /** @brief fill with random values */ - void fill_random(double log2bound = 0); - /** @brief raw pointer access */ - double* data(); - /** @brief raw pointer access (const version) */ - const double* data() const; - /** @brief content hashcode */ - thash content_hash() const; -}; - -/** @brief test layout for the VMP_PMAT */ -class fft64_rnx_vmp_pmat_layout { - public: - const uint64_t nn; - const uint64_t nrows; - const uint64_t ncols; - RNX_VMP_PMAT* const data; - fft64_rnx_vmp_pmat_layout(uint64_t n, uint64_t nrows, uint64_t ncols); - double* get_addr(uint64_t row, uint64_t col, uint64_t blk) const; - reim4_elem get(uint64_t row, uint64_t col, uint64_t blk) const; - thash content_hash() const; - reim4_elem get_zext(uint64_t row, uint64_t col, uint64_t blk) const; - reim_fft64vec get_zext(uint64_t row, uint64_t col) const; - void set(uint64_t row, uint64_t col, uint64_t blk, const reim4_elem& v) const; - void set(uint64_t row, uint64_t col, const reim_fft64vec& value); - /** @brief fill with random double values (unstructured) */ - void fill_random(double log2bound); - ~fft64_rnx_vmp_pmat_layout(); -}; - -/** @brief test layout for the SVP_PPOL */ -class fft64_rnx_svp_ppol_layout { - public: - const uint64_t nn; - RNX_SVP_PPOL* const data; - fft64_rnx_svp_ppol_layout(uint64_t n); - thash content_hash() const; - reim_fft64vec get_copy() const; - void set(const reim_fft64vec&); - /** @brief fill with random double values (unstructured) */ - void fill_random(double log2bound); - /** @brief fill with random ffts of small int polynomials */ - void fill_dft_random(uint64_t log2bound); - ~fft64_rnx_svp_ppol_layout(); -}; -#endif // SPQLIOS_EXT_VEC_RNX_LAYOUT_H
D spqlios/lib/test/testlib/zn_layouts.cpp

@@ -1,55 +0,0 @@

-#include "zn_layouts.h" - -zn32_pmat_layout::zn32_pmat_layout(uint64_t nrows, uint64_t ncols) - : nrows(nrows), // - ncols(ncols), // - data((ZN32_VMP_PMAT*)malloc((nrows * ncols + 7) * sizeof(int32_t))) {} - -zn32_pmat_layout::~zn32_pmat_layout() { free(data); } - -int32_t* zn32_pmat_layout::get_addr(uint64_t row, uint64_t col) const { - REQUIRE_DRAMATICALLY(row < nrows, "row overflow" << row << " / " << nrows); - REQUIRE_DRAMATICALLY(col < ncols, "col overflow" << col << " / " << ncols); - const uint64_t nblk = ncols >> 5; - const uint64_t rem_ncols = ncols & 31; - uint64_t blk = col >> 5; - uint64_t col_rem = col & 31; - if (blk < nblk) { - // column is part of a full block - return (int32_t*)data + blk * nrows * 32 + row * 32 + col_rem; - } else { - // column is part of the last block - return (int32_t*)data + blk * nrows * 32 + row * rem_ncols + col_rem; - } -} -int32_t zn32_pmat_layout::get(uint64_t row, uint64_t col) const { return *get_addr(row, col); } -int32_t zn32_pmat_layout::get_zext(uint64_t row, uint64_t col) const { - if (row >= nrows || col >= ncols) return 0; - return *get_addr(row, col); -} -void zn32_pmat_layout::set(uint64_t row, uint64_t col, int32_t value) { *get_addr(row, col) = value; } -void zn32_pmat_layout::fill_random() { - int32_t* d = (int32_t*)data; - for (uint64_t i = 0; i < nrows * ncols; ++i) d[i] = uniform_i64_bits(32); -} -thash zn32_pmat_layout::content_hash() const { return test_hash(data, nrows * ncols * sizeof(int32_t)); } - -template <typename T> -std::vector<int32_t> vmp_product(const T* vec, uint64_t vec_size, uint64_t out_size, const zn32_pmat_layout& mat) { - uint64_t rows = std::min(vec_size, mat.nrows); - uint64_t cols = std::min(out_size, mat.ncols); - std::vector<int32_t> res(out_size, 0); - for (uint64_t j = 0; j < cols; ++j) { - for (uint64_t i = 0; i < rows; ++i) { - res[j] += vec[i] * mat.get(i, j); - } - } - return res; -} - -template std::vector<int32_t> vmp_product(const int8_t* vec, uint64_t vec_size, uint64_t out_size, - const zn32_pmat_layout& mat); -template std::vector<int32_t> vmp_product(const int16_t* vec, uint64_t vec_size, uint64_t out_size, - const zn32_pmat_layout& mat); -template std::vector<int32_t> vmp_product(const int32_t* vec, uint64_t vec_size, uint64_t out_size, - const zn32_pmat_layout& mat);
D spqlios/lib/test/testlib/zn_layouts.h

@@ -1,29 +0,0 @@

-#ifndef SPQLIOS_EXT_ZN_LAYOUTS_H -#define SPQLIOS_EXT_ZN_LAYOUTS_H - -#include "../../spqlios/arithmetic/zn_arithmetic.h" -#include "test_commons.h" - -class zn32_pmat_layout { - public: - const uint64_t nrows; - const uint64_t ncols; - ZN32_VMP_PMAT* const data; - zn32_pmat_layout(uint64_t nrows, uint64_t ncols); - - private: - int32_t* get_addr(uint64_t row, uint64_t col) const; - - public: - int32_t get(uint64_t row, uint64_t col) const; - int32_t get_zext(uint64_t row, uint64_t col) const; - void set(uint64_t row, uint64_t col, int32_t value); - void fill_random(); - thash content_hash() const; - ~zn32_pmat_layout(); -}; - -template <typename T> -std::vector<int32_t> vmp_product(const T* vec, uint64_t vec_size, uint64_t out_size, const zn32_pmat_layout& mat); - -#endif // SPQLIOS_EXT_ZN_LAYOUTS_H