Skip to content
Merged
Show file tree
Hide file tree
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: 1 addition & 1 deletion .github/workflows/cpp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Packages
run: |
sudo apt-get update
sudo apt-get install zlib1g-dev libzstd-dev cppcheck clang-tidy clang-format llvm-15 llvm-15-dev
sudo apt-get install zlib1g-dev libzstd-dev cppcheck clang-tidy clang-format libc++-dev llvm-15 llvm-15-dev
- name: Code quality - fmt
run: ./tool/format.sh check
- name: Code quality - cppcheck
Expand Down
13 changes: 12 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,14 @@ option(NYXSTONE_SANITIZERS "Enable sanitizers" OFF)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")

if(DEFINED ENV{NYXSTONE_LLVM_PREFIX})
# Ignore LLVM_ROOT variables
# Ignore LLVM_ROOT and system search paths so that a user-supplied prefix
# is the only place find_package(LLVM ...) looks. Without these,
# find_package would walk system paths and could pick up a different
# LLVM version (e.g., a newer one matching the iterated version) instead
# of the one the user pinned.
set(CMAKE_FIND_USE_PACKAGE_ROOT_PATH OFF)
set(CMAKE_FIND_USE_CMAKE_SYSTEM_PATH OFF)
set(CMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH OFF)
set(CMAKE_PREFIX_PATH $ENV{NYXSTONE_LLVM_PREFIX})
endif()

Expand Down Expand Up @@ -79,6 +85,11 @@ if(NYXSTONE_BUILD_EXAMPLES)
nyxstone::nyxstone
)

add_executable(benchmark examples/benchmark.cpp)
target_link_libraries(benchmark PRIVATE
nyxstone::nyxstone
)

include(CTest)
add_test(NAME TestExample COMMAND $<TARGET_FILE:example>)
add_test(NAME TestCLI COMMAND "${CMAKE_CURRENT_LIST_DIR}/tool/test-cli.sh")
Expand Down
21 changes: 14 additions & 7 deletions bindings/rust/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,15 @@ const ENV_FORCE_FFI_LINKING: &str = "NYXSTONE_LINK_FFI";
fn main() {
let headers = [
"nyxstone/include/nyxstone.h",
"nyxstone/src/ELFStreamerWrapper.h",
"nyxstone/src/ObjectWriterWrapper.h",
"nyxstone/src/FastStreamer.h",
"nyxstone/src/Target/AArch64/MCTargetDesc/AArch64FixupKinds.h",
"nyxstone/src/Target/AArch64/MCTargetDesc/AArch64MCExpr.h",
"src/nyxstone_ffi.hpp",
];

let sources = [
"nyxstone/src/nyxstone.cpp",
"nyxstone/src/ObjectWriterWrapper.cpp",
"nyxstone/src/ELFStreamerWrapper.cpp",
"nyxstone/src/FastStreamer.cpp",
"src/nyxstone_ffi.cpp",
];

Expand Down Expand Up @@ -134,8 +132,8 @@ fn search_llvm_config() -> Result<PathBuf> {
let version = version.parse::<u32>().context("Parsing LLVM version")?;

ensure!(
(15..=18).contains(&version),
"LLVM major version is {}, must be 15-18.",
(15..=20).contains(&version),
"LLVM major version is {}, must be 15-20.",
version
);

Expand Down Expand Up @@ -198,7 +196,10 @@ fn target_os_is(name: &str) -> bool {

/// Return an iterator over possible names for the llvm-config binary.
fn llvm_config_binary_names() -> impl Iterator<Item = String> {
let base_names = (15..=18)
// Newest-first so $PATH can carry multiple llvm-config-N binaries without
// the older one masking the newest the user actually has installed.
let base_names = (15..=20)
.rev()
.flat_map(|version| {
[
format!("llvm-config-{}", version),
Expand Down Expand Up @@ -475,6 +476,12 @@ fn get_link_libraries(llvm_config_path: &Path, preferences: &LinkingPreferences)
fn extract_library(s: &str, kind: LibraryKind) -> Vec<String> {
s.split(&[' ', '\n'] as &[char])
.filter(|s| !s.is_empty())
// Polly is an LLVM polyhedral optimizer that nyxstone does not use,
// but `llvm-config --libnames` lists it. Ubuntu's `llvm-N-dev` apt
// package ships the dynamic Polly libs but not the static archives,
// so a default `cargo build` (which prefers static linking) fails
// with `could not find native static library Polly`. Skip them.
.filter(|s| !s.starts_with("libPolly") && !s.starts_with("Polly"))
.map(|name| {
// --libnames gives library filenames. Extract only the name that
// we need to pass to the linker.
Expand Down
213 changes: 213 additions & 0 deletions bindings/rust/examples/benchmark.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
//! Cross-language counterpart to `examples/benchmark.cpp`.
//!
//! Same architectures, same instructions, same package sizes (1 and 10), same
//! measurement methodology and output layout — so output from this binary can
//! be compared directly against the C++ benchmark (e.g. `diff` after dropping
//! the `[C++]` / `[Rust]` tag on the first line).
//!
//! Run with: `cargo run --release --example benchmark -- [seconds_per_measurement]`

extern crate anyhow;
extern crate nyxstone;

use std::env;
use std::time::{Duration, Instant};

use anyhow::Result;
use nyxstone::{Nyxstone, NyxstoneConfig};

struct ArchConfig {
name: &'static str,
triple: &'static str,
instructions: &'static [&'static str],
}

const ARCHES: &[ArchConfig] = &[
ArchConfig {
name: "x86_64",
triple: "x86_64-linux-gnu",
instructions: &[
"mov rax, rbx",
"xor rax, rax",
"add rsp, 8",
"sub rsp, 8",
"push rbx",
"pop rbx",
"inc rax",
"dec rax",
"and rax, rbx",
"or rax, rbx",
],
},
ArchConfig {
name: "x86_32",
triple: "i686-linux-gnu",
instructions: &[
"mov eax, ebx",
"xor eax, eax",
"add esp, 8",
"sub esp, 8",
"push ebx",
"pop ebx",
"inc eax",
"dec eax",
"and eax, ebx",
"or eax, ebx",
],
},
ArchConfig {
name: "aarch64",
triple: "aarch64-linux-gnueabihf",
instructions: &[
"mov x0, x1",
"add x0, x0, #1",
"sub x0, x0, #1",
"mov x1, x2",
"add x1, x1, #1",
"sub x1, x1, #1",
"mov x2, x3",
"add sp, sp, #16",
"sub sp, sp, #16",
"ret",
],
},
ArchConfig {
name: "armv8m",
triple: "armv8m.main-none-eabi",
instructions: &[
"mov r0, r1",
"add r0, r0, #1",
"sub r0, r0, #1",
"mov r1, r2",
"add r1, r1, #1",
"sub r1, r1, #1",
"mov r2, r3",
"nop",
"push {r0}",
"pop {r0}",
],
},
];

const PACKAGE_SIZES: &[usize] = &[1, 10];

fn make_package(arch: &ArchConfig, package_size: usize) -> String {
let mut out = String::new();
for i in 0..package_size {
if i > 0 {
out.push_str("; ");
}
out.push_str(arch.instructions[i % arch.instructions.len()]);
}
out
}

fn run_for_at_least<F: FnMut()>(target: Duration, mut f: F) -> f64 {
for _ in 0..10 {
f();
}

let mut count: usize = 0;
let start = Instant::now();
loop {
for _ in 0..50 {
f();
count += 1;
}
let elapsed = start.elapsed();
if elapsed >= target {
return count as f64 / elapsed.as_secs_f64();
}
}
}

fn fmt_rate(value: f64) -> String {
let body = if value >= 1e9 {
format!("{:.2} G", value / 1e9)
} else if value >= 1e6 {
format!("{:.2} M", value / 1e6)
} else if value >= 1e3 {
format!("{:.2} k", value / 1e3)
} else {
format!("{:.2} ", value)
};
format!("{:>10}", body)
}

fn run(arch: &ArchConfig, target: Duration) {
println!("\n=== {} ({}) ===", arch.name, arch.triple);

let nyxstone = match Nyxstone::new(arch.triple, NyxstoneConfig::default()) {
Ok(n) => n,
Err(e) => {
eprintln!(" [skipped] {e}");
return;
}
};

for &package_size in PACKAGE_SIZES {
let assembly = make_package(arch, package_size);

let bytes = match nyxstone.assemble(&assembly, 0x1000) {
Ok(b) => b,
Err(e) => {
eprintln!(" [skipped {package_size}] {e}");
continue;
}
};

let insn_count = nyxstone
.assemble_to_instructions(&assembly, 0x1000)
.map(|v| v.len())
.unwrap_or(package_size);

let asm_ops_per_s = run_for_at_least(target, || {
let _ = nyxstone.assemble(&assembly, 0x1000);
});
let dis_ops_per_s = run_for_at_least(target, || {
let _ = nyxstone.disassemble(&bytes, 0x1000, 0);
});

println!(
" Package {:>2} ({} insns, {} bytes)",
package_size,
insn_count,
bytes.len()
);
println!(
" assemble :{} ops/s {} insns/s {} bytes/s",
fmt_rate(asm_ops_per_s),
fmt_rate(asm_ops_per_s * insn_count as f64),
fmt_rate(asm_ops_per_s * bytes.len() as f64),
);
println!(
" disassemble :{} ops/s {} insns/s {} bytes/s",
fmt_rate(dis_ops_per_s),
fmt_rate(dis_ops_per_s * insn_count as f64),
fmt_rate(dis_ops_per_s * bytes.len() as f64),
);
}
}

fn main() -> Result<()> {
let args: Vec<String> = env::args().collect();
let mut seconds_per_bench: f64 = 0.5;
if args.len() > 1 {
match args[1].parse::<f64>() {
Ok(v) if v > 0.0 => seconds_per_bench = v,
_ => {
eprintln!("Usage: {} [seconds_per_measurement]", args[0]);
std::process::exit(1);
}
}
}
let target = Duration::from_secs_f64(seconds_per_bench);

print!("Nyxstone benchmark [Rust] (target {seconds_per_bench}s per measurement)");

for arch in ARCHES {
run(arch, target);
}

Ok(())
}
2 changes: 1 addition & 1 deletion bindings/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ impl Nyxstone {
/// - `assembly`: The instructions to assemble.
/// - `address`: The start location of the instructions.
/// - `labels`: Additional label definitions by absolute address, expects a reference to some `Map<AsRef<str>, u64>`
/// which can be iterated over.
/// which can be iterated over.
///
/// # Returns:
/// Ok() and bytecode on success, Err() otherwise.
Expand Down
10 changes: 4 additions & 6 deletions bindings/rust/src/nyxstone_ffi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@ ByteResult NyxstoneFFI::assemble(
{
std::vector<Nyxstone::LabelDefinition> cpp_labels {};
cpp_labels.reserve(labels.size());
std::transform(std::begin(labels), std::end(labels), std::back_inserter(cpp_labels), [](const auto& label) {
return Nyxstone::LabelDefinition { std::string(label.name), label.address };
});
std::transform(std::begin(labels), std::end(labels), std::back_inserter(cpp_labels),
[](const auto& label) { return Nyxstone::LabelDefinition { std::string(label.name), label.address }; });

auto result = nyxstone->assemble(std::string { assembly }, address, cpp_labels).map([](const auto& cpp_bytes) {
rust::Vec<uint8_t> bytes {};
Expand All @@ -59,9 +58,8 @@ InstructionResult NyxstoneFFI::assemble_to_instructions(
{
std::vector<Nyxstone::LabelDefinition> cpp_labels;
cpp_labels.reserve(labels.size());
std::transform(std::begin(labels), std::end(labels), std::back_inserter(cpp_labels), [](const auto& label) {
return Nyxstone::LabelDefinition { std::string(label.name), label.address };
});
std::transform(std::begin(labels), std::end(labels), std::back_inserter(cpp_labels),
[](const auto& label) { return Nyxstone::LabelDefinition { std::string(label.name), label.address }; });
std::vector<Nyxstone::Instruction> cpp_instructions {};

auto result = nyxstone->assemble_to_instructions(std::string { assembly }, address, cpp_labels)
Expand Down
Loading
Loading