Skip to content

Commit

Permalink
Intrinsic test tool to compare neon intrinsics with C (#1170)
Browse files Browse the repository at this point in the history
  • Loading branch information
JamieCunliffe committed Sep 9, 2021
1 parent 7fd93e3 commit 1a7d0ce
Show file tree
Hide file tree
Showing 13 changed files with 5,672 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -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
Expand Up @@ -4,6 +4,7 @@ members = [
"crates/core_arch",
"crates/std_detect",
"crates/stdarch-gen",
"crates/intrinsic-test",
"examples/"
]
exclude = [
Expand Down
6 changes: 5 additions & 1 deletion ci/docker/aarch64-unknown-linux-gnu/Dockerfile
@@ -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" \
Expand Down
4 changes: 3 additions & 1 deletion ci/run-docker.sh
Expand Up @@ -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 \
Expand All @@ -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 \
Expand Down
7 changes: 7 additions & 0 deletions ci/run.sh
Expand Up @@ -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
Expand Down Expand Up @@ -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
(
Expand Down
16 changes: 16 additions & 0 deletions crates/intrinsic-test/Cargo.toml
@@ -0,0 +1,16 @@
[package]
name = "intrinsic-test"
version = "0.1.0"
authors = ["Jamie Cunliffe <Jamie.Cunliffe@arm.com>"]
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
@@ -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
@@ -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
@@ -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",
)),
}
}

0 comments on commit 1a7d0ce

Please sign in to comment.