Skip to content

Intrinsic test tool to compare neon intrinsics with C #1170

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 9, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -4,3 +4,5 @@ target
tags
crates/stdarch-gen/aarch64.rs
crates/stdarch-gen/arm.rs
c_programs/*
rust_programs/*
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ members = [
"crates/core_arch",
"crates/std_detect",
"crates/stdarch-gen",
"crates/intrinsic-test",
"examples/"
]
exclude = [
6 changes: 5 additions & 1 deletion ci/docker/aarch64-unknown-linux-gnu/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
g++ \
ca-certificates \
libc6-dev \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu \
libc6-dev-arm64-cross \
qemu-user \
make \
file
file \
clang-12 \
lld

ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUNNER="qemu-aarch64 -L /usr/aarch64-linux-gnu" \
4 changes: 3 additions & 1 deletion ci/run-docker.sh
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ run() {
target=$(echo "${1}" | sed 's/-emulated//')
echo "Building docker container for TARGET=${1}"
docker build -t stdarch -f "ci/docker/${1}/Dockerfile" ci/
mkdir -p target
mkdir -p target c_programs rust_programs
echo "Running docker"
# shellcheck disable=SC2016
docker run \
@@ -29,6 +29,8 @@ run() {
--volume "$(rustc --print sysroot)":/rust:ro \
--volume "$(pwd)":/checkout:ro \
--volume "$(pwd)"/target:/checkout/target \
--volume "$(pwd)"/c_programs:/checkout/c_programs \
--volume "$(pwd)"/rust_programs:/checkout/rust_programs \
--init \
--workdir /checkout \
--privileged \
7 changes: 7 additions & 0 deletions ci/run.sh
Original file line number Diff line number Diff line change
@@ -65,6 +65,8 @@ cargo_test() {
CORE_ARCH="--manifest-path=crates/core_arch/Cargo.toml"
STD_DETECT="--manifest-path=crates/std_detect/Cargo.toml"
STDARCH_EXAMPLES="--manifest-path=examples/Cargo.toml"
INTRINSIC_TEST="--manifest-path=crates/intrinsic-test/Cargo.toml"

cargo_test "${CORE_ARCH} --release"

if [ "$NOSTD" != "1" ]; then
@@ -111,6 +113,11 @@ case ${TARGET} in

esac

if [ "${TARGET}" = "aarch64-unknown-linux-gnu" ]; then
export CPPFLAGS="-fuse-ld=lld -I/usr/aarch64-linux-gnu/include/ -I/usr/aarch64-linux-gnu/include/c++/9/aarch64-linux-gnu/"
cargo run ${INTRINSIC_TEST} --release --bin intrinsic-test -- crates/intrinsic-test/neon-intrinsics.csv --runner "${CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUNNER}" --cppcompiler "clang++-12"
fi

if [ "$NORUN" != "1" ] && [ "$NOSTD" != 1 ]; then
# Test examples
(
16 changes: 16 additions & 0 deletions crates/intrinsic-test/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "intrinsic-test"
version = "0.1.0"
authors = ["Jamie Cunliffe <[email protected]>"]
edition = "2018"

[dependencies]
lazy_static = "1.4.0"
serde = { version = "1", features = ["derive"] }
csv = "1.1"
clap = "2.33.3"
regex = "1.4.2"
log = "0.4.11"
pretty_env_logger = "0.4.0"
rayon = "1.5.0"
diff = "0.1.12"
24 changes: 24 additions & 0 deletions crates/intrinsic-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
Generate and run programs using equivalent C and Rust intrinsics, checking that
each produces the same result from random inputs.

# Usage
```
USAGE:
intrinsic-test [OPTIONS] <INPUT>
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
--cppcompiler <CPPCOMPILER> The C++ compiler to use for compiling the c++ code [default: clang++]
--runner <RUNNER> Run the C programs under emulation with this command
--toolchain <TOOLCHAIN> The rust toolchain to use for building the rust code
ARGS:
<INPUT> The input file containing the intrinsics
```

The intrinsic.csv is the arm neon tracking google sheet (https://docs.google.com/spreadsheets/d/1MqW1g8c7tlhdRWQixgdWvR4uJHNZzCYAf4V0oHjZkwA/edit#gid=0)
that contains the intrinsic list. The done percentage column should be renamed to "enabled".

4,356 changes: 4,356 additions & 0 deletions crates/intrinsic-test/neon-intrinsics.csv

Large diffs are not rendered by default.

137 changes: 137 additions & 0 deletions crates/intrinsic-test/src/argument.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use serde::{Deserialize, Deserializer};

use crate::types::IntrinsicType;
use crate::Language;

/// An argument for the intrinsic.
#[derive(Debug, PartialEq, Clone)]
pub struct Argument {
/// The argument's index in the intrinsic function call.
pub pos: usize,
/// The argument name.
pub name: String,
/// The type of the argument.
pub ty: IntrinsicType,
}

impl Argument {
/// Creates an argument from a Rust style signature i.e. `name: type`
fn from_rust(pos: usize, arg: &str) -> Result<Self, String> {
let mut parts = arg.split(':');
let name = parts.next().unwrap().trim().to_string();
let ty = IntrinsicType::from_rust(parts.next().unwrap().trim())?;

Ok(Self { pos, name, ty })
}

fn to_c_type(&self) -> String {
self.ty.c_type()
}

fn is_simd(&self) -> bool {
self.ty.is_simd()
}

pub fn is_ptr(&self) -> bool {
self.ty.is_ptr()
}
}

#[derive(Debug, PartialEq, Clone)]
pub struct ArgumentList {
pub args: Vec<Argument>,
}

impl ArgumentList {
/// Creates an argument list from a Rust function signature, the data for
/// this function should only be the arguments.
/// e.g. for `fn test(a: u32, b: u32) -> u32` data should just be `a: u32, b: u32`
fn from_rust_arguments(data: &str) -> Result<Self, String> {
let args = data
.split(',')
.enumerate()
.map(|(idx, arg)| Argument::from_rust(idx, arg))
.collect::<Result<_, _>>()?;

Ok(Self { args })
}

/// Converts the argument list into the call paramters for a C function call.
/// e.g. this would generate something like `a, &b, c`
pub fn as_call_param_c(&self) -> String {
self.args
.iter()
.map(|arg| match arg.ty {
IntrinsicType::Ptr { .. } => {
format!("&{}", arg.name)
}
IntrinsicType::Type { .. } => arg.name.clone(),
})
.collect::<Vec<String>>()
.join(", ")
}

/// Converts the argument list into the call paramters for a Rust function.
/// e.g. this would generate something like `a, b, c`
pub fn as_call_param_rust(&self) -> String {
self.args
.iter()
.map(|arg| arg.name.clone())
.collect::<Vec<String>>()
.join(", ")
}

/// Creates a line that initializes this argument for C code.
/// e.g. `int32x2_t a = { 0x1, 0x2 };`
pub fn init_random_values_c(&self, pass: usize) -> String {
self.iter()
.map(|arg| {
format!(
"{ty} {name} = {{ {values} }};",
ty = arg.to_c_type(),
name = arg.name,
values = arg.ty.populate_random(pass, &Language::C)
)
})
.collect::<Vec<_>>()
.join("\n ")
}

/// Creates a line that initializes this argument for Rust code.
/// e.g. `let a = transmute([0x1, 0x2]);`
pub fn init_random_values_rust(&self, pass: usize) -> String {
self.iter()
.map(|arg| {
if arg.is_simd() {
format!(
"let {name} = ::std::mem::transmute([{values}]);",
name = arg.name,
values = arg.ty.populate_random(pass, &Language::Rust),
)
} else {
format!(
"let {name} = {value};",
name = arg.name,
value = arg.ty.populate_random(pass, &Language::Rust)
)
}
})
.collect::<Vec<_>>()
.join("\n ")
}

pub fn iter(&self) -> std::slice::Iter<'_, Argument> {
self.args.iter()
}
}

impl<'de> Deserialize<'de> for ArgumentList {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
let s = String::deserialize(deserializer)?;
Self::from_rust_arguments(&s).map_err(Error::custom)
}
}
112 changes: 112 additions & 0 deletions crates/intrinsic-test/src/intrinsic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use crate::types::{IntrinsicType, TypeKind};

use super::argument::ArgumentList;
use serde::de::Unexpected;
use serde::{de, Deserialize, Deserializer};

/// An intrinsic
#[derive(Deserialize, Debug, PartialEq, Clone)]
pub struct Intrinsic {
/// If the intrinsic should be tested.
#[serde(deserialize_with = "bool_from_string")]
pub enabled: bool,

/// The function name of this intrinsic.
pub name: String,

/// Any arguments for this intrinsinc.
#[serde(rename = "args")]
pub arguments: ArgumentList,

/// The return type of this intrinsic.
#[serde(rename = "return")]
pub results: IntrinsicType,
}

impl Intrinsic {
/// Generates a std::cout for the intrinsics results that will match the
/// rust debug output format for the return type.
pub fn print_result_c(&self, index: usize) -> String {
let lanes = if self.results.num_lanes() > 1 {
(0..self.results.num_lanes())
.map(|idx| -> std::string::String {
format!(
"{cast}{lane_fn}(__return_value, {lane})",
cast = self.results.c_promotion(),
lane_fn = self.results.get_lane_function(),
lane = idx
)
})
.collect::<Vec<_>>()
.join(r#" << ", " << "#)
} else {
format!(
"{promote}cast<{cast}>(__return_value)",
cast = match self.results.kind() {
TypeKind::Float if self.results.inner_size() == 32 => "float".to_string(),
TypeKind::Float if self.results.inner_size() == 64 => "double".to_string(),
TypeKind::Int => format!("int{}_t", self.results.inner_size()),
TypeKind::UInt => format!("uint{}_t", self.results.inner_size()),
TypeKind::Poly => format!("poly{}_t", self.results.inner_size()),
ty => todo!("print_result_c - Unknown type: {:#?}", ty),
},
promote = self.results.c_promotion(),
)
};

format!(
r#"std::cout << "Result {idx}: {ty}" << std::fixed << std::setprecision(150) << {lanes} << "{close}" << std::endl;"#,
ty = if self.results.is_simd() {
format!("{}(", self.results.c_type())
} else {
String::from("")
},
close = if self.results.is_simd() { ")" } else { "" },
lanes = lanes,
idx = index,
)
}

pub fn generate_pass_rust(&self, index: usize) -> String {
format!(
r#"
unsafe {{
{initialized_args}
let res = {intrinsic_call}({args});
println!("Result {idx}: {{:.150?}}", res);
}}"#,
initialized_args = self.arguments.init_random_values_rust(index),
intrinsic_call = self.name,
args = self.arguments.as_call_param_rust(),
idx = index,
)
}

pub fn generate_pass_c(&self, index: usize) -> String {
format!(
r#" {{
{initialized_args}
auto __return_value = {intrinsic_call}({args});
{print_result}
}}"#,
initialized_args = self.arguments.init_random_values_c(index),
intrinsic_call = self.name,
args = self.arguments.as_call_param_c(),
print_result = self.print_result_c(index)
)
}
}

fn bool_from_string<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
D: Deserializer<'de>,
{
match String::deserialize(deserializer)?.to_uppercase().as_ref() {
"TRUE" => Ok(true),
"FALSE" => Ok(false),
other => Err(de::Error::invalid_value(
Unexpected::Str(other),
&"TRUE or FALSE",
)),
}
}
400 changes: 400 additions & 0 deletions crates/intrinsic-test/src/main.rs

Large diffs are not rendered by default.

483 changes: 483 additions & 0 deletions crates/intrinsic-test/src/types.rs

Large diffs are not rendered by default.

126 changes: 126 additions & 0 deletions crates/intrinsic-test/src/values.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/// Gets a hex constant value for a single lane in in a determistic way
/// * `bits`: The number of bits for the type, only 8, 16, 32, 64 are valid values
/// * `simd`: The index of the simd lane we are generating for
/// * `pass`: The index of the pass we are generating the values for
pub fn values_for_pass(bits: u32, simd: u32, pass: usize) -> String {
let index = pass + (simd as usize);

if bits == 8 {
format!("{:#X}", VALUES_8[index % VALUES_8.len()])
} else if bits == 16 {
format!("{:#X}", VALUES_16[index % VALUES_16.len()])
} else if bits == 32 {
format!("{:#X}", VALUES_32[index % VALUES_32.len()])
} else if bits == 64 {
format!("{:#X}", VALUES_64[index % VALUES_64.len()])
} else {
panic!("Unknown size: {}", bits);
}
}

pub const VALUES_8: &[u8] = &[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0xf0, 0x80, 0x3b, 0xff,
];

pub const VALUES_16: &[u16] = &[
0x0000, // 0.0
0x0400, // The smallest normal value.
0x37ff, // The value just below 0.5.
0x3800, // 0.5
0x3801, // The value just above 0.5.
0x3bff, // The value just below 1.0.
0x3c00, // 1.0
0x3c01, // The value just above 1.0.
0x3e00, // 1.5
0x4900, // 10
0x7bff, // The largest finite value.
0x7c00, // Infinity.
// NaNs.
// - Quiet NaNs
0x7f23, 0x7e00, // - Signalling NaNs
0x7d23, 0x7c01, // Subnormals.
// - A recognisable bit pattern.
0x0012, // - The largest subnormal value.
0x03ff, // - The smallest subnormal value.
0x0001, // The same values again, but negated.
0x8000, 0x8400, 0xb7ff, 0xb800, 0xb801, 0xbbff, 0xbc00, 0xbc01, 0xbe00, 0xc900, 0xfbff, 0xfc00,
0xff23, 0xfe00, 0xfd23, 0xfc01, 0x8012, 0x83ff, 0x8001,
];

pub const VALUES_32: &[u32] = &[
// Simple values.
0x00000000, // 0.0
0x00800000, // The smallest normal value.
0x3effffff, // The value just below 0.5.
0x3f000000, // 0.5
0x3f000001, // The value just above 0.5.
0x3f7fffff, // The value just below 1.0.
0x3f800000, // 1.0
0x3f800001, // The value just above 1.0.
0x3fc00000, // 1.5
0x41200000, // 10
0x7f8fffff, // The largest finite value.
0x7f800000, // Infinity.
// NaNs.
// - Quiet NaNs
0x7fd23456, 0x7fc00000, // - Signalling NaNs
0x7f923456, 0x7f800001, // Subnormals.
// - A recognisable bit pattern.
0x00123456, // - The largest subnormal value.
0x007fffff, // - The smallest subnormal value.
0x00000001, // The same values again, but negated.
0x80000000, 0x80800000, 0xbeffffff, 0xbf000000, 0xbf000001, 0xbf7fffff, 0xbf800000, 0xbf800001,
0xbfc00000, 0xc1200000, 0xff8fffff, 0xff800000, 0xffd23456, 0xffc00000, 0xff923456, 0xff800001,
0x80123456, 0x807fffff, 0x80000001,
];

pub const VALUES_64: &[u64] = &[
// Simple values.
0x0000000000000000, // 0.0
0x0010000000000000, // The smallest normal value.
0x3fdfffffffffffff, // The value just below 0.5.
0x3fe0000000000000, // 0.5
0x3fe0000000000001, // The value just above 0.5.
0x3fefffffffffffff, // The value just below 1.0.
0x3ff0000000000000, // 1.0
0x3ff0000000000001, // The value just above 1.0.
0x3ff8000000000000, // 1.5
0x4024000000000000, // 10
0x7fefffffffffffff, // The largest finite value.
0x7ff0000000000000, // Infinity.
// NaNs.
// - Quiet NaNs
0x7ff923456789abcd,
0x7ff8000000000000,
// - Signalling NaNs
0x7ff123456789abcd,
0x7ff0000000000000,
// Subnormals.
// - A recognisable bit pattern.
0x000123456789abcd,
// - The largest subnormal value.
0x000fffffffffffff,
// - The smallest subnormal value.
0x0000000000000001,
// The same values again, but negated.
0x8000000000000000,
0x8010000000000000,
0xbfdfffffffffffff,
0xbfe0000000000000,
0xbfe0000000000001,
0xbfefffffffffffff,
0xbff0000000000000,
0xbff0000000000001,
0xbff8000000000000,
0xc024000000000000,
0xffefffffffffffff,
0xfff0000000000000,
0xfff923456789abcd,
0xfff8000000000000,
0xfff123456789abcd,
0xfff0000000000000,
0x800123456789abcd,
0x800fffffffffffff,
0x8000000000000001,
];