├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── changelog.md ├── i24_benches ├── Cargo.toml ├── analysis.py ├── benchmark_analysis │ ├── benchmark_report.md │ ├── operation_durations.png │ ├── performance_by_category.png │ ├── relative_performance.png │ └── throughput_comparison.png ├── i24_benchmark_results.json └── src │ └── main.rs ├── logo.png └── src ├── lib.rs └── repr.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | i24_benches/target -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "i24" 3 | version = "2.1.0" 4 | edition = "2021" 5 | license = "MIT" 6 | description = "A Rust library for working with 24-bit integers." 7 | readme = "README.md" 8 | repository = "https://github.com/jmg049/i24" 9 | documentation = "https://docs.rs/i24" 10 | categories = ["data-structures", "mathematics", "encoding"] 11 | 12 | [dependencies] 13 | bytemuck = "1" 14 | num-traits = "0.2" 15 | 16 | serde = { version = "1", default-features = false, optional = true } 17 | pyo3 = { version = "0.24.2", features = ["extension-module"], optional = true } 18 | numpy = { version = "0.24.0", optional = true } 19 | 20 | [dev-dependencies] 21 | serde = { version = "1", features = ["derive"] } 22 | serde_json = { version = "1" } 23 | 24 | [features] 25 | alloc = [] 26 | std = [] 27 | pyo3 = ["std", "dep:pyo3", "dep:numpy"] 28 | serde = ["dep:serde"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024-2024 Jack Geraghty 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # i24: A 24-bit Signed Integer Type for Rust 4 | 5 | i24 Logo 6 | 7 | [![Crates.io](https://img.shields.io/crates/v/i24.svg)](https://crates.io/crates/i24)[![Docs.rs](https://docs.rs/i24/badge.svg)](https://docs.rs/i24)![MSRV: 1.70+](https://img.shields.io/badge/MSRV-1.70+-blue) 8 |
9 | 10 | The ``i24`` crate provides a 24-bit signed integer type for Rust, filling the gap between 11 | ``i16`` and ``i32``. This type is particularly useful in audio processing, certain embedded 12 | systems, and other scenarios where 24-bit precision is required but 32 bits would be excessive. 13 | 14 | ## Features 15 | 16 | - Efficient 24-bit signed integer representation 17 | - Seamless conversion to and from ``i32`` 18 | - Support for basic arithmetic operations with overflow checking 19 | - Bitwise operations 20 | - Conversions from various byte representations (little-endian, big-endian, native) 21 | - Implements common traits like``Debug``,``Display``,``PartialEq``,``Eq``,``PartialOrd``,``Ord``, and``Hash`` 22 | 23 | This crate came about as a part of the [Wavers](https://crates.io/crates/wavers) project, which is a Wav file reader and writer for Rust. 24 | The ``i24`` struct also has pyo3 bindings for use in Python. Enable the``pyo3`` feature to use the pyo3 bindings. 25 | 26 | ## Usage 27 | 28 | Add this to your`` Cargo.toml`: 29 | 30 | ````toml 31 | [dependencies] 32 | i24 = "2.1.0" 33 | ```` 34 | 35 | Then, in your Rust code: 36 | 37 | ````rust 38 | 39 | # #[macro_use] extern crate i24 40 | 41 | let a = i24!(1000); 42 | let b = i24!(2000); 43 | let c = a + b; 44 | assert_eq!(c.to_i32(), 3000); 45 | assert_eq!(c, i24!(3000)); 46 | ```` 47 | 48 | The``i24!`` macro allows you to create``i24`` values at compile time, ensuring that the value is within the valid range. 49 | 50 | Then if working with 3-byte representations from disk or the network, you can use the``I24DiskMethods`` trait to read and write ``i24`` slices of ``i24`. 51 | 52 | ````ignore 53 | use i24::I24DiskMethods; // Bring extension trait into scope 54 | use i24::i24 as I24; // Import the i24 type 55 | let raw_data: &[u8] = &[0x00, 0x01, 0x02, 0x00, 0x01, 0xFF]; // 2 values 56 | let values: Vec = I24::read_i24s_be(raw_data).expect("valid buffer"); 57 | 58 | let encoded: Vec = I24::write_i24s_be(&values); 59 | assert_eq!(encoded, raw_data); 60 | ```` 61 | 62 | ## Safety and Limitations 63 | 64 | While``i24`` strives to behave similarly to Rust's built-in integer types, there are some 65 | important considerations: 66 | 67 | - The valid range for ``i24`` is [-8,388,608, 8,388,607]. 68 | - Overflow behavior in arithmetic operations matches that of ``i32`. 69 | - Bitwise operations are performed on the 24-bit representation. 70 | 71 | Always use checked arithmetic operations when dealing with untrusted input or when 72 | overflow/underflow is a concern. 73 | 74 | ``i24`` aligns with the safety requirements of bytemuck (``NoUninit``, ``Zeroable`` and ``AnyBitPattern``), ensuring that it is safe to use for converting between valid bytes and a ``i24`` value. 75 | Then when using the ``I24DiskMethods`` trait, it is safe to use (internally) the ``bytemuck::cast_slice`` function to convert between a slice of bytes and a slice of ``i24`` values. 76 | 77 | ## Feature Flags 78 | 79 | - **pyo3**: Enables the pyo3 bindings for the ``i24`` type. 80 | - **serde**: Enables the ``Serialize`` and ``Deserialize`` traits for the ``i24`` type. 81 | - **alloc**: Enables the ``I24DiskMethods`` trait for the ``i24`` type. 82 | 83 | ## Contributing 84 | 85 | Contributions are welcome! Please feel free to submit a Pull Request. 86 | 87 | ## License 88 | 89 | This project is licensed under MIT - see the [LICENSE](https://github.com/jmg049/i24/blob/main/LICENSE) file for details. 90 | 91 | ## Benchmarks 92 | 93 | The crate was tested using the code found in the [i24_benches](./i24_benches) directory of the repo. The full benchmark data can be found in the [benchmark report](./i24_benches/benchmark_analysis/benchmark_report.md). 94 | Below is a figure which summarises the performance with repsect to the ``i32`` type. From the figure it is clear that the ``i24`` type mostly matches the performance of an ``i32`` with some slight variations. 95 | 96 | ![Durations overview per operation](i24_benches/benchmark_analysis/operation_durations.png) 97 | 98 | ## Related Projects 99 | 100 | This crate was developed as part of the [Wavers](https://crates.io/crates/wavers) project, a Wav file reader and writer for Rust. 101 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.1.0] – 2025-04-30 4 | 5 | ### Added 6 | 7 | - **Disk deserialization API** via the new `I24DiskMethods` trait: 8 | - `read_i24s_{be, le, ne}()` for safe parsing of `&[u8]` into `Vec` 9 | - `read_i24s_{be, le, ne}_unchecked()` for fast unchecked variants (assumes length multiple of 3) 10 | - `write_i24s_{be, le, ne}()` for writing a `&[i24]` as `Vec` in 3-byte format 11 | - Implemented internal `DiskI24` type to enable zero-copy deserialization using `bytemuck::cast_slice` 12 | 13 | ### Changed 14 | 15 | - Expanded documentation for the `i24` type: 16 | - Describes memory layout, safety guarantees, `NoUninit` compatibility 17 | - Details disk I/O patterns and endian-aware methods 18 | - Marked the crate as **safe for use with `bytemuck::NoUninit`** and explained limitations 19 | - Updated README 20 | 21 | ### Removed 22 | 23 | - Removed bytemuck `Pod` trait for `i24` struct. 24 | -------------------------------------------------------------------------------- /i24_benches/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "i24_benches" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | i24 = {path = "../"} 8 | chrono = "0.4.41" 9 | rand = "0.9.1" 10 | rustc_version_runtime = "0.3.0" 11 | serde = { version = "1.0.219", features = ["derive"] } 12 | serde_json = "1.0.140" 13 | sys-info = "0.9.1" 14 | 15 | 16 | 17 | [profile.release] 18 | lto = true 19 | codegen-units = 1 20 | opt-level = 3 21 | panic = "abort" 22 | strip = true -------------------------------------------------------------------------------- /i24_benches/analysis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Comprehensive analysis and visualization of i24 benchmark results. 4 | 5 | This script processes the JSON output from the i24 benchmark tool, 6 | generates detailed visualizations, and produces formatted markdown tables 7 | for easy inclusion in documentation or reports. 8 | 9 | Make sure to have the required libraries installed: 10 | ``` 11 | pip install pandas matplotlib seaborn tabulate numpy 12 | ``` 13 | """ 14 | 15 | import json 16 | import matplotlib.pyplot as plt 17 | import pandas as pd 18 | import numpy as np 19 | import seaborn as sns 20 | from pathlib import Path 21 | import argparse 22 | from tabulate import tabulate 23 | import sys 24 | from datetime import datetime 25 | import re 26 | 27 | # Set Seaborn style for better-looking plots 28 | sns.set_theme(style="whitegrid") 29 | 30 | def parse_args(): 31 | """Parse command line arguments.""" 32 | parser = argparse.ArgumentParser(description="Analyze i24 benchmark results") 33 | parser.add_argument("--input", "-i", default="i24_benchmark_results.json", 34 | help="Path to benchmark results JSON file") 35 | parser.add_argument("--output", "-o", default="benchmark_analysis", 36 | help="Output directory for reports and visualizations") 37 | parser.add_argument("--format", "-f", choices=["png", "svg", "pdf"], default="png", 38 | help="Output format for visualizations") 39 | parser.add_argument("--dpi", type=int, default=300, 40 | help="DPI for output images") 41 | return parser.parse_args() 42 | 43 | def load_benchmark_data(filepath): 44 | """Load benchmark data from JSON file.""" 45 | try: 46 | with open(filepath, 'r') as f: 47 | data = json.load(f) 48 | return data 49 | except FileNotFoundError: 50 | print(f"Error: Benchmark file {filepath} not found.") 51 | sys.exit(1) 52 | except json.JSONDecodeError: 53 | print(f"Error: Failed to parse {filepath} as JSON.") 54 | sys.exit(1) 55 | 56 | def preprocess_data(data): 57 | """Process raw benchmark data into useful DataFrames.""" 58 | # Convert results to a DataFrame 59 | df = pd.DataFrame(data["results"]) 60 | 61 | # Extract operation categories 62 | df['operation_category'] = df['operation'].apply(categorize_operation) 63 | 64 | # Create pivot tables for different analyses 65 | pivot_throughput = df.pivot(index='operation', columns='operand_type', values='throughput') 66 | pivot_duration = df.pivot(index='operation', columns='operand_type', values='duration_ns') 67 | 68 | # Calculate performance ratios 69 | performance_df = pd.DataFrame() 70 | 71 | # Throughput ratio (higher is better for i24) 72 | if 'i24' in pivot_throughput.columns and 'i32' in pivot_throughput.columns: 73 | performance_df['throughput_ratio'] = pivot_throughput['i24'] / pivot_throughput['i32'] 74 | 75 | # Duration ratio (lower is better for i24) 76 | if 'i24' in pivot_duration.columns and 'i32' in pivot_duration.columns: 77 | performance_df['duration_ratio'] = pivot_duration['i24'] / pivot_duration['i32'] 78 | 79 | # Add operation categories 80 | operation_categories = {op: categorize_operation(op) for op in performance_df.index} 81 | performance_df['category'] = performance_df.index.map(operation_categories) 82 | 83 | return { 84 | 'raw': df, 85 | 'throughput': pivot_throughput, 86 | 'duration': pivot_duration, 87 | 'performance': performance_df, 88 | 'system_info': data['system_info'], 89 | 'timestamp': data['timestamp'] 90 | } 91 | 92 | def categorize_operation(operation_name): 93 | """Categorize operations into groups.""" 94 | if re.match(r'Add|Sub|Mul|Div|Rem', operation_name): 95 | return 'Arithmetic' 96 | elif re.match(r'BitAnd|BitOr|BitXor|BitwiseNot', operation_name): 97 | return 'Bitwise' 98 | elif re.match(r'LeftShift|RightShift', operation_name): 99 | return 'Shift' 100 | elif re.match(r'Neg', operation_name): 101 | return 'Unary' 102 | elif re.match(r'FromI32|ToI32|ByteConversion', operation_name): 103 | return 'Conversion' 104 | else: 105 | return 'Other' 106 | 107 | def plot_throughput_comparison(data, output_dir, img_format, dpi): 108 | """Create a bar chart comparing throughput between i24 and i32.""" 109 | throughput_df = data['throughput'] 110 | 111 | # Filter only operations that have both i24 and i32 implementations 112 | operations = [] 113 | for op in throughput_df.index: 114 | if 'i24' in throughput_df.columns and 'i32' in throughput_df.columns: 115 | if not pd.isna(throughput_df.loc[op, 'i24']) and not pd.isna(throughput_df.loc[op, 'i32']): 116 | operations.append(op) 117 | 118 | if not operations: 119 | print("Warning: No comparable operations found for throughput") 120 | return 121 | 122 | # Categorize operations 123 | categories = [categorize_operation(op) for op in operations] 124 | category_colors = { 125 | 'Arithmetic': '#1f77b4', # blue 126 | 'Bitwise': '#ff7f0e', # orange 127 | 'Shift': '#2ca02c', # green 128 | 'Unary': '#d62728', # red 129 | 'Conversion': '#9467bd', # purple 130 | 'Other': '#8c564b' # brown 131 | } 132 | 133 | i24_colors = [category_colors[cat] for cat in categories] 134 | i32_colors = [sns.desaturate(color, 0.6) for color in i24_colors] 135 | 136 | # Set up the figure 137 | plt.figure(figsize=(14, 8)) 138 | 139 | # Create positions for the bars 140 | x = np.arange(len(operations)) 141 | width = 0.35 142 | 143 | # Create bars 144 | i24_throughput = [throughput_df.loc[op, 'i24'] for op in operations] 145 | i32_throughput = [throughput_df.loc[op, 'i32'] for op in operations] 146 | 147 | # Plot bars 148 | plt.bar(x - width/2, i24_throughput, width, label='i24', color=i24_colors) 149 | plt.bar(x + width/2, i32_throughput, width, label='i32', color=i32_colors) 150 | 151 | # Add labels and title 152 | plt.ylabel('Operations per second', fontsize=12) 153 | plt.title('Throughput Comparison: i24 vs i32', fontsize=14) 154 | plt.xticks(x, operations, rotation=45, ha='right') 155 | plt.legend(fontsize=12) 156 | 157 | # Add a grid for better readability 158 | plt.grid(axis='y', linestyle='--', alpha=0.7) 159 | 160 | # Format y-axis with commas for thousands 161 | plt.gca().yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'{x:,.0f}')) 162 | 163 | # Add values above bars 164 | for i, (i24_val, i32_val) in enumerate(zip(i24_throughput, i32_throughput)): 165 | plt.text(i - width/2, i24_val + max(i24_throughput) * 0.02, f'{i24_val:,.0f}', 166 | ha='center', va='bottom', fontsize=8, rotation=45) 167 | plt.text(i + width/2, i32_val + max(i32_throughput) * 0.02, f'{i32_val:,.0f}', 168 | ha='center', va='bottom', fontsize=8, rotation=45) 169 | 170 | # Adjust layout and save 171 | plt.tight_layout() 172 | plt.savefig(f"{output_dir}/throughput_comparison.{img_format}", dpi=dpi) 173 | print(f"Created throughput comparison chart: {output_dir}/throughput_comparison.{img_format}") 174 | plt.close() 175 | 176 | def plot_relative_performance(data, output_dir, img_format, dpi): 177 | """Create a plot showing relative performance of i24 compared to i32.""" 178 | performance_df = data['performance'] 179 | 180 | if 'throughput_ratio' not in performance_df.columns: 181 | print("Warning: No throughput ratio data available") 182 | return 183 | 184 | # Sort operations by category and then by performance ratio 185 | sorted_df = performance_df.sort_values(['category', 'throughput_ratio'], ascending=[True, False]) 186 | 187 | # Drop NaN values 188 | sorted_df = sorted_df.dropna(subset=['throughput_ratio']) 189 | 190 | if sorted_df.empty: 191 | print("Warning: No valid data for performance ratio plot") 192 | return 193 | 194 | # Set up plot 195 | plt.figure(figsize=(12, 10)) 196 | 197 | # Create horizontal bar chart 198 | operations = sorted_df.index 199 | ratios = sorted_df['throughput_ratio'] 200 | categories = sorted_df['category'] 201 | 202 | # Define colors based on performance and category 203 | category_colors = { 204 | 'Arithmetic': '#1f77b4', # blue 205 | 'Bitwise': '#ff7f0e', # orange 206 | 'Shift': '#2ca02c', # green 207 | 'Unary': '#d62728', # red 208 | 'Conversion': '#9467bd', # purple 209 | 'Other': '#8c564b' # brown 210 | } 211 | 212 | colors = [category_colors[cat] if r >= 1.0 else sns.desaturate(category_colors[cat], 0.6) 213 | for cat, r in zip(categories, ratios)] 214 | 215 | # Create y-position for each operation 216 | y_pos = np.arange(len(operations)) 217 | 218 | # Plot horizontal bars 219 | bars = plt.barh(y_pos, ratios, color=colors) 220 | 221 | # Add a vertical line at 1.0 222 | plt.axvline(x=1.0, color='black', linestyle='--', alpha=0.7) 223 | 224 | # Add labels 225 | plt.xlabel('Performance Ratio (i24 / i32)', fontsize=12) 226 | plt.title('Relative Performance of i24 compared to i32', fontsize=14) 227 | plt.yticks(y_pos, operations) 228 | 229 | # Add actual values as text 230 | for i, v in enumerate(ratios): 231 | text_color = 'black' if v < 1.0 else 'white' 232 | plt.text(max(v + 0.05, 1.05), i, f"{v:.2f}x", va='center', color=text_color) 233 | 234 | # Add category dividers and labels 235 | last_category = None 236 | category_positions = [] 237 | 238 | for i, (op, cat) in enumerate(zip(operations, categories)): 239 | if cat != last_category: 240 | if i > 0: 241 | plt.axhline(y=i-0.5, color='gray', linestyle='-', alpha=0.3, linewidth=1) 242 | last_category = cat 243 | category_positions.append((i, cat)) 244 | 245 | # Add category labels 246 | for pos, cat in category_positions: 247 | plt.text(-0.15, pos, cat, rotation=90, ha='center', va='bottom', 248 | fontweight='bold', color=category_colors[cat]) 249 | 250 | # Adjust layout and save 251 | plt.tight_layout() 252 | plt.subplots_adjust(left=0.2) # Make room for category labels 253 | plt.savefig(f"{output_dir}/relative_performance.{img_format}", dpi=dpi) 254 | print(f"Created relative performance chart: {output_dir}/relative_performance.{img_format}") 255 | plt.close() 256 | 257 | def plot_performance_by_category(data, output_dir, img_format, dpi): 258 | """Create a boxplot showing performance distribution by operation category.""" 259 | performance_df = data['performance'].copy() 260 | 261 | if 'throughput_ratio' not in performance_df.columns or 'category' not in performance_df.columns: 262 | print("Warning: Missing data for category performance plot") 263 | return 264 | 265 | # Drop NaN values 266 | performance_df = performance_df.dropna(subset=['throughput_ratio']) 267 | 268 | if performance_df.empty: 269 | print("Warning: No valid data for category performance plot") 270 | return 271 | 272 | # Set up plot 273 | plt.figure(figsize=(10, 6)) 274 | 275 | # Create boxplot 276 | sns.boxplot(x='category', y='throughput_ratio', data=performance_df) 277 | 278 | # Add a horizontal line at 1.0 279 | plt.axhline(y=1.0, color='red', linestyle='--', alpha=0.7) 280 | 281 | # Add labels 282 | plt.xlabel('Operation Category', fontsize=12) 283 | plt.ylabel('Performance Ratio (i24 / i32)', fontsize=12) 284 | plt.title('Performance Distribution by Operation Category', fontsize=14) 285 | 286 | # Adjust layout and save 287 | plt.tight_layout() 288 | plt.savefig(f"{output_dir}/performance_by_category.{img_format}", dpi=dpi) 289 | print(f"Created category performance chart: {output_dir}/performance_by_category.{img_format}") 290 | plt.close() 291 | 292 | def plot_operation_durations(data, output_dir, img_format, dpi): 293 | """Create a log-scale plot comparing operation durations.""" 294 | duration_df = data['duration'].copy() 295 | 296 | # Filter only operations that have both i24 and i32 implementations 297 | operations = [] 298 | for op in duration_df.index: 299 | if 'i24' in duration_df.columns and 'i32' in duration_df.columns: 300 | if not pd.isna(duration_df.loc[op, 'i24']) and not pd.isna(duration_df.loc[op, 'i32']): 301 | operations.append(op) 302 | 303 | if not operations: 304 | print("Warning: No comparable operations found for duration plot") 305 | return 306 | 307 | # Convert to nanoseconds per operation for easier comparison 308 | duration_df = duration_df.loc[operations] 309 | 310 | # Set up plot 311 | plt.figure(figsize=(12, 8)) 312 | 313 | # Create log-scale plot 314 | plt.semilogy(duration_df.index, duration_df['i24'], 'o-', label='i24') 315 | plt.semilogy(duration_df.index, duration_df['i32'], 'o-', label='i32') 316 | 317 | # Add labels 318 | plt.ylabel('Duration per Operation (ns, log scale)', fontsize=12) 319 | plt.title('Operation Duration Comparison (lower is better)', fontsize=14) 320 | plt.xticks(rotation=45, ha='right') 321 | plt.legend() 322 | 323 | # Add grid for better readability 324 | plt.grid(True, which="both", ls="-", alpha=0.2) 325 | 326 | # Adjust layout and save 327 | plt.tight_layout() 328 | plt.savefig(f"{output_dir}/operation_durations.{img_format}", dpi=dpi) 329 | print(f"Created operation durations chart: {output_dir}/operation_durations.{img_format}") 330 | plt.close() 331 | 332 | def generate_markdown_report(data, output_dir): 333 | """Generate a comprehensive markdown report with benchmark results.""" 334 | system_info = data['system_info'] 335 | performance_df = data['performance'].copy() 336 | throughput_df = data['throughput'].copy() 337 | duration_df = data['duration'] 338 | 339 | # Format DataFrames for the report 340 | if not performance_df.empty: 341 | # Format the ratio columns 342 | if 'throughput_ratio' in performance_df.columns: 343 | performance_df['throughput_ratio'] = performance_df['throughput_ratio'].map(lambda x: f"{x:.2f}x" if not pd.isna(x) else np.nan) 344 | if 'duration_ratio' in performance_df.columns: 345 | performance_df['duration_ratio'] = performance_df['duration_ratio'].map(lambda x: f"{x:.2f}x" if not pd.isna(x) else np.nan) 346 | 347 | # Format throughput values with commas 348 | for col in throughput_df.columns: 349 | throughput_df[col] = throughput_df[col].map(lambda x: f"{x:,.0f}" if not pd.isna(x) else np.nan) 350 | 351 | # Format duration values 352 | for col in duration_df.columns: 353 | duration_df[col] = duration_df[col].map(lambda x: f"{x:.3f}" if not pd.isna(x) else np.nan) 354 | 355 | # Create the markdown content 356 | markdown = [] 357 | 358 | # Title and metadata 359 | markdown.extend([ 360 | "# i24 Benchmark Results\n", 361 | f"*Generated on: {data['timestamp']}*\n", 362 | "## System Information\n", 363 | f"- **CPU:** {system_info['cpu_info']}", 364 | f"- **Memory:** {system_info['memory_mb']} MB", 365 | f"- **OS:** {system_info['os']}", 366 | f"- **Rust Version:** {system_info['rust_version']}\n", 367 | ]) 368 | 369 | # Executive summary 370 | if 'throughput_ratio' in performance_df.columns: 371 | better_count = sum(performance_df['throughput_ratio'].str.rstrip('x').astype(float) >= 1.0) 372 | worse_count = sum(performance_df['throughput_ratio'].str.rstrip('x').astype(float) < 1.0) 373 | best_op = performance_df['throughput_ratio'].str.rstrip('x').astype(float).idxmax() 374 | best_ratio = performance_df.loc[best_op, 'throughput_ratio'] 375 | worst_op = performance_df['throughput_ratio'].str.rstrip('x').astype(float).idxmin() 376 | worst_ratio = performance_df.loc[worst_op, 'throughput_ratio'] 377 | 378 | markdown.extend([ 379 | "## Executive Summary\n", 380 | f"- i24 performs better than i32 in **{better_count}** operations and worse in **{worse_count}** operations.", 381 | f"- Best relative performance: **{best_op}** ({best_ratio} of i32 performance)", 382 | f"- Worst relative performance: **{worst_op}** ({worst_ratio} of i32 performance)\n", 383 | ]) 384 | 385 | # Performance by category 386 | if 'category' in performance_df.columns and 'throughput_ratio' in performance_df.columns: 387 | markdown.append("## Performance by Category\n") 388 | 389 | # Group by category and calculate average performance 390 | category_perf = performance_df.copy() 391 | category_perf['ratio_value'] = category_perf['throughput_ratio'].str.rstrip('x').astype(float) 392 | category_avg = category_perf.groupby('category')['ratio_value'].mean().sort_values(ascending=False) 393 | 394 | # Create category summary table 395 | category_table = [] 396 | for cat, avg in category_avg.items(): 397 | category_table.append([cat, f"{avg:.2f}x"]) 398 | 399 | markdown.append(tabulate(category_table, headers=["Category", "Average Performance"], tablefmt="pipe")) 400 | markdown.append("\n") 401 | 402 | # Detailed benchmark results 403 | markdown.append("## Detailed Benchmark Results\n") 404 | 405 | # Throughput table 406 | markdown.append("### Throughput (Operations per second)\n") 407 | throughput_reset = throughput_df.reset_index() 408 | markdown.append(tabulate(throughput_reset, headers='keys', tablefmt="pipe", showindex=False)) 409 | markdown.append("\n") 410 | 411 | # Duration table 412 | markdown.append("### Duration (nanoseconds per operation)\n") 413 | duration_reset = duration_df.reset_index() 414 | markdown.append(tabulate(duration_reset, headers='keys', tablefmt="pipe", showindex=False)) 415 | markdown.append("\n") 416 | 417 | # Relative performance table 418 | markdown.append("### Relative Performance (i24 vs i32)\n") 419 | if not performance_df.empty: 420 | perf_reset = performance_df.reset_index()[['operation', 'throughput_ratio', 'duration_ratio', 'category']] 421 | perf_reset.columns = ['Operation', 'Throughput Ratio', 'Duration Ratio', 'Category'] 422 | markdown.append(tabulate(perf_reset, headers='keys', tablefmt="pipe", showindex=False)) 423 | markdown.append("\n") 424 | 425 | # Best and worst performers 426 | markdown.append("## Performance Highlights\n") 427 | 428 | if 'throughput_ratio' in performance_df.columns: 429 | # Convert string ratios back to float for sorting 430 | performance_df['ratio_value'] = performance_df['throughput_ratio'].str.rstrip('x').astype(float) 431 | 432 | # Top 5 best performers 433 | markdown.append("### Top 5 Best Performers\n") 434 | top5 = performance_df.sort_values('ratio_value', ascending=False).head(5) 435 | top5_table = top5.reset_index()[['operation', 'throughput_ratio', 'category']] 436 | top5_table.columns = ['Operation', 'Performance Ratio', 'Category'] 437 | markdown.append(tabulate(top5_table, headers='keys', tablefmt="pipe", showindex=False)) 438 | markdown.append("\n") 439 | 440 | # Bottom 5 worst performers 441 | markdown.append("### Bottom 5 Worst Performers\n") 442 | bottom5 = performance_df.sort_values('ratio_value', ascending=True).head(5) 443 | bottom5_table = bottom5.reset_index()[['operation', 'throughput_ratio', 'category']] 444 | bottom5_table.columns = ['Operation', 'Performance Ratio', 'Category'] 445 | markdown.append(tabulate(bottom5_table, headers='keys', tablefmt="pipe", showindex=False)) 446 | markdown.append("\n") 447 | 448 | # Write the report to file 449 | report_path = f"{output_dir}/benchmark_report.md" 450 | with open(report_path, "w") as f: 451 | f.write("\n".join(markdown)) 452 | 453 | print(f"Generated markdown report: {report_path}") 454 | 455 | return "\n".join(markdown) 456 | 457 | def main(): 458 | """Main function to process benchmark data, create visualizations and reports.""" 459 | # Parse command line arguments 460 | args = parse_args() 461 | 462 | # Create output directory 463 | output_dir = args.output 464 | Path(output_dir).mkdir(exist_ok=True, parents=True) 465 | 466 | print(f"Loading benchmark data from {args.input}...") 467 | data = load_benchmark_data(args.input) 468 | 469 | print("Processing benchmark data...") 470 | processed_data = preprocess_data(data) 471 | 472 | print("Generating visualizations...") 473 | plot_throughput_comparison(processed_data, output_dir, args.format, args.dpi) 474 | plot_relative_performance(processed_data, output_dir, args.format, args.dpi) 475 | plot_performance_by_category(processed_data, output_dir, args.format, args.dpi) 476 | plot_operation_durations(processed_data, output_dir, args.format, args.dpi) 477 | 478 | print("Generating markdown report...") 479 | report = generate_markdown_report(processed_data, output_dir) 480 | 481 | print(f"Analysis complete! Results saved to {output_dir}/") 482 | 483 | if __name__ == "__main__": 484 | main() -------------------------------------------------------------------------------- /i24_benches/benchmark_analysis/benchmark_report.md: -------------------------------------------------------------------------------- 1 | # i24 Benchmark Results 2 | 3 | *Generated on: 2025-04-29T12:16:37.682921433+01:00* 4 | 5 | ## System Information 6 | 7 | - **CPU:** 24 cores 8 | - **Memory:** 64136 MB 9 | - **OS:** Linux 6.11.0-24-generic 10 | - **Rust Version:** 1.85.0 11 | 12 | ## Executive Summary 13 | 14 | ![Durations overview per operation](./operation_durations.png) 15 | 16 | - i24 performs better than i32 in **7** operations and worse in **5** operations. 17 | - Best relative performance: **LeftShift** (1.12x of i32 performance) 18 | - Worst relative performance: **Remainder** (0.35x of i32 performance) 19 | 20 | ## Performance by Category 21 | 22 | | Category | Average Performance | 23 | |:-----------|:----------------------| 24 | | Shift | 1.01x | 25 | | Bitwise | 1.01x | 26 | | Unary | 0.88x | 27 | | Arithmetic | 0.81x | 28 | 29 | ## Detailed Benchmark Results 30 | 31 | ### Throughput (Operations per second) 32 | 33 | | operation | i24 | i32 | 34 | |:---------------|:------------------------|:------------------------| 35 | | Addition | 27,100,268,292,682,928 | 43,290,038,961,038,960 | 36 | | BitAnd | 42,735,038,461,538,464 | 44,247,783,185,840,704 | 37 | | BitOr | 44,247,783,185,840,704 | 44,444,440,000,000,000 | 38 | | BitXor | 44,247,783,185,840,704 | 44,444,440,000,000,000 | 39 | | BitwiseNot | 625,000,000,000,000,000 | 588,235,294,117,647,104 | 40 | | ByteConversion | 666,666,666,666,666,752 | nan | 41 | | Division | 2,056,374,132 | 2,050,133,261 | 42 | | FromI32 | 666,666,666,666,666,752 | nan | 43 | | LeftShift | 588,235,294,117,647,104 | 526,315,789,473,684,160 | 44 | | Multiplication | 43,478,256,521,739,128 | 42,918,450,643,776,824 | 45 | | Negation | 588,235,294,117,647,104 | 666,666,666,666,666,752 | 46 | | Remainder | 863,742,668 | 2,473,845,159 | 47 | | RightShift | 500,000,000,000,000,000 | 555,555,555,555,555,584 | 48 | | Subtraction | 44,247,783,185,840,704 | 41,841,000,000,000,000 | 49 | | ToI32 | 526,315,789,473,684,160 | nan | 50 | 51 | 52 | ### Duration (nanoseconds per operation) 53 | 54 | | operation | i24 | i32 | 55 | |:---------------|--------------:|--------------:| 56 | | Addition | 369 | 231 | 57 | | BitAnd | 234 | 226 | 58 | | BitOr | 226 | 225 | 59 | | BitXor | 226 | 225 | 60 | | BitwiseNot | 16 | 17 | 61 | | ByteConversion | 15 | nan | 62 | | Division | 4.86293e+09 | 4.87773e+09 | 63 | | FromI32 | 15 | nan | 64 | | LeftShift | 17 | 19 | 65 | | Multiplication | 230 | 233 | 66 | | Negation | 17 | 15 | 67 | | Remainder | 1.15775e+10 | 4.04229e+09 | 68 | | RightShift | 20 | 18 | 69 | | Subtraction | 226 | 239 | 70 | | ToI32 | 19 | nan | 71 | 72 | 73 | ### Relative Performance (i24 vs i32) 74 | 75 | | Operation | Throughput Ratio | Duration Ratio | Category | 76 | |:---------------|:-------------------|:-----------------|:-----------| 77 | | Addition | 0.63x | 1.60x | Arithmetic | 78 | | BitAnd | 0.97x | 1.04x | Bitwise | 79 | | BitOr | 1.00x | 1.00x | Bitwise | 80 | | BitXor | 1.00x | 1.00x | Bitwise | 81 | | BitwiseNot | 1.06x | 0.94x | Bitwise | 82 | | ByteConversion | nan | nan | Conversion | 83 | | Division | 1.00x | 1.00x | Arithmetic | 84 | | FromI32 | nan | nan | Conversion | 85 | | LeftShift | 1.12x | 0.89x | Shift | 86 | | Multiplication | 1.01x | 0.99x | Arithmetic | 87 | | Negation | 0.88x | 1.13x | Unary | 88 | | Remainder | 0.35x | 2.86x | Arithmetic | 89 | | RightShift | 0.90x | 1.11x | Shift | 90 | | Subtraction | 1.06x | 0.95x | Arithmetic | 91 | | ToI32 | nan | nan | Conversion | 92 | 93 | 94 | ## Performance Highlights 95 | 96 | ### Top 5 Best Performers 97 | 98 | | Operation | Performance Ratio | Category | 99 | |:---------------|:--------------------|:-----------| 100 | | LeftShift | 1.12x | Shift | 101 | | BitwiseNot | 1.06x | Bitwise | 102 | | Subtraction | 1.06x | Arithmetic | 103 | | Multiplication | 1.01x | Arithmetic | 104 | | Division | 1.00x | Arithmetic | 105 | 106 | 107 | ### Bottom 5 Worst Performers 108 | 109 | | Operation | Performance Ratio | Category | 110 | |:------------|:--------------------|:-----------| 111 | | Remainder | 0.35x | Arithmetic | 112 | | Addition | 0.63x | Arithmetic | 113 | | Negation | 0.88x | Unary | 114 | | RightShift | 0.90x | Shift | 115 | | BitAnd | 0.97x | Bitwise | 116 | 117 | -------------------------------------------------------------------------------- /i24_benches/benchmark_analysis/operation_durations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmg049/i24/4d0a7483ec2fb72ccec9524c5b3b1d8a63a44b2e/i24_benches/benchmark_analysis/operation_durations.png -------------------------------------------------------------------------------- /i24_benches/benchmark_analysis/performance_by_category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmg049/i24/4d0a7483ec2fb72ccec9524c5b3b1d8a63a44b2e/i24_benches/benchmark_analysis/performance_by_category.png -------------------------------------------------------------------------------- /i24_benches/benchmark_analysis/relative_performance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmg049/i24/4d0a7483ec2fb72ccec9524c5b3b1d8a63a44b2e/i24_benches/benchmark_analysis/relative_performance.png -------------------------------------------------------------------------------- /i24_benches/benchmark_analysis/throughput_comparison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmg049/i24/4d0a7483ec2fb72ccec9524c5b3b1d8a63a44b2e/i24_benches/benchmark_analysis/throughput_comparison.png -------------------------------------------------------------------------------- /i24_benches/i24_benchmark_results.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "2025-04-29T12:16:37.682921433+01:00", 3 | "system_info": { 4 | "cpu_info": "24 cores", 5 | "memory_mb": 64136, 6 | "os": "Linux 6.11.0-24-generic", 7 | "rust_version": "1.85.0" 8 | }, 9 | "results": [ 10 | { 11 | "operation": "Addition", 12 | "operand_type": "i24", 13 | "iterations": 1000, 14 | "duration_ns": 369, 15 | "throughput": 2.710026829268293e16 16 | }, 17 | { 18 | "operation": "Addition", 19 | "operand_type": "i32", 20 | "iterations": 1000, 21 | "duration_ns": 231, 22 | "throughput": 4.329003896103896e16 23 | }, 24 | { 25 | "operation": "Subtraction", 26 | "operand_type": "i24", 27 | "iterations": 1000, 28 | "duration_ns": 226, 29 | "throughput": 4.42477831858407e16 30 | }, 31 | { 32 | "operation": "Subtraction", 33 | "operand_type": "i32", 34 | "iterations": 1000, 35 | "duration_ns": 239, 36 | "throughput": 4.1841e16 37 | }, 38 | { 39 | "operation": "Multiplication", 40 | "operand_type": "i24", 41 | "iterations": 1000, 42 | "duration_ns": 230, 43 | "throughput": 4.347825652173913e16 44 | }, 45 | { 46 | "operation": "Multiplication", 47 | "operand_type": "i32", 48 | "iterations": 1000, 49 | "duration_ns": 233, 50 | "throughput": 4.2918450643776824e16 51 | }, 52 | { 53 | "operation": "Division", 54 | "operand_type": "i24", 55 | "iterations": 1000, 56 | "duration_ns": 4862927833, 57 | "throughput": 2056374131.67838 58 | }, 59 | { 60 | "operation": "Division", 61 | "operand_type": "i32", 62 | "iterations": 1000, 63 | "duration_ns": 4877731213, 64 | "throughput": 2050133261.412246 65 | }, 66 | { 67 | "operation": "Remainder", 68 | "operand_type": "i24", 69 | "iterations": 1000, 70 | "duration_ns": 11577521136, 71 | "throughput": 863742668.4461205 72 | }, 73 | { 74 | "operation": "Remainder", 75 | "operand_type": "i32", 76 | "iterations": 1000, 77 | "duration_ns": 4042289779, 78 | "throughput": 2473845158.7391753 79 | }, 80 | { 81 | "operation": "BitAnd", 82 | "operand_type": "i24", 83 | "iterations": 1000, 84 | "duration_ns": 234, 85 | "throughput": 4.273503846153846e16 86 | }, 87 | { 88 | "operation": "BitAnd", 89 | "operand_type": "i32", 90 | "iterations": 1000, 91 | "duration_ns": 226, 92 | "throughput": 4.42477831858407e16 93 | }, 94 | { 95 | "operation": "BitOr", 96 | "operand_type": "i24", 97 | "iterations": 1000, 98 | "duration_ns": 226, 99 | "throughput": 4.42477831858407e16 100 | }, 101 | { 102 | "operation": "BitOr", 103 | "operand_type": "i32", 104 | "iterations": 1000, 105 | "duration_ns": 225, 106 | "throughput": 4.444444e16 107 | }, 108 | { 109 | "operation": "BitXor", 110 | "operand_type": "i24", 111 | "iterations": 1000, 112 | "duration_ns": 226, 113 | "throughput": 4.42477831858407e16 114 | }, 115 | { 116 | "operation": "BitXor", 117 | "operand_type": "i32", 118 | "iterations": 1000, 119 | "duration_ns": 225, 120 | "throughput": 4.444444e16 121 | }, 122 | { 123 | "operation": "LeftShift", 124 | "operand_type": "i24", 125 | "iterations": 1000, 126 | "duration_ns": 17, 127 | "throughput": 5.882352941176471e17 128 | }, 129 | { 130 | "operation": "LeftShift", 131 | "operand_type": "i32", 132 | "iterations": 1000, 133 | "duration_ns": 19, 134 | "throughput": 5.2631578947368416e17 135 | }, 136 | { 137 | "operation": "RightShift", 138 | "operand_type": "i24", 139 | "iterations": 1000, 140 | "duration_ns": 20, 141 | "throughput": 5e17 142 | }, 143 | { 144 | "operation": "RightShift", 145 | "operand_type": "i32", 146 | "iterations": 1000, 147 | "duration_ns": 18, 148 | "throughput": 5.555555555555556e17 149 | }, 150 | { 151 | "operation": "Negation", 152 | "operand_type": "i24", 153 | "iterations": 1000, 154 | "duration_ns": 17, 155 | "throughput": 5.882352941176471e17 156 | }, 157 | { 158 | "operation": "Negation", 159 | "operand_type": "i32", 160 | "iterations": 1000, 161 | "duration_ns": 15, 162 | "throughput": 6.666666666666668e17 163 | }, 164 | { 165 | "operation": "BitwiseNot", 166 | "operand_type": "i24", 167 | "iterations": 1000, 168 | "duration_ns": 16, 169 | "throughput": 6.25e17 170 | }, 171 | { 172 | "operation": "BitwiseNot", 173 | "operand_type": "i32", 174 | "iterations": 1000, 175 | "duration_ns": 17, 176 | "throughput": 5.882352941176471e17 177 | }, 178 | { 179 | "operation": "ToI32", 180 | "operand_type": "i24", 181 | "iterations": 1000, 182 | "duration_ns": 19, 183 | "throughput": 5.2631578947368416e17 184 | }, 185 | { 186 | "operation": "FromI32", 187 | "operand_type": "i24", 188 | "iterations": 1000, 189 | "duration_ns": 15, 190 | "throughput": 6.666666666666668e17 191 | }, 192 | { 193 | "operation": "ByteConversion", 194 | "operand_type": "i24", 195 | "iterations": 1000, 196 | "duration_ns": 15, 197 | "throughput": 6.666666666666668e17 198 | } 199 | ] 200 | } -------------------------------------------------------------------------------- /i24_benches/src/main.rs: -------------------------------------------------------------------------------- 1 | use i24::i24; 2 | use std::time::{Duration, Instant}; 3 | use serde::{Serialize, Deserialize}; 4 | use std::fs::File; 5 | use std::io::Write; 6 | use rand::{Rng, SeedableRng}; 7 | use rand::rngs::StdRng; 8 | 9 | // Define the operations we want to benchmark 10 | #[derive(Debug, Clone, Copy, Serialize, Deserialize)] 11 | enum Operation { 12 | Add, 13 | Sub, 14 | Mul, 15 | Div, 16 | Rem, 17 | BitAnd, 18 | BitOr, 19 | BitXor, 20 | Shl, 21 | Shr, 22 | Neg, 23 | Not, 24 | FromI32, 25 | ToI32, 26 | ByteConversion, 27 | } 28 | 29 | impl std::fmt::Display for Operation { 30 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 31 | match self { 32 | Operation::Add => write!(f, "Addition"), 33 | Operation::Sub => write!(f, "Subtraction"), 34 | Operation::Mul => write!(f, "Multiplication"), 35 | Operation::Div => write!(f, "Division"), 36 | Operation::Rem => write!(f, "Remainder"), 37 | Operation::BitAnd => write!(f, "BitAnd"), 38 | Operation::BitOr => write!(f, "BitOr"), 39 | Operation::BitXor => write!(f, "BitXor"), 40 | Operation::Shl => write!(f, "LeftShift"), 41 | Operation::Shr => write!(f, "RightShift"), 42 | Operation::Neg => write!(f, "Negation"), 43 | Operation::Not => write!(f, "BitwiseNot"), 44 | Operation::FromI32 => write!(f, "FromI32"), 45 | Operation::ToI32 => write!(f, "ToI32"), 46 | Operation::ByteConversion => write!(f, "ByteConversion"), 47 | } 48 | } 49 | } 50 | 51 | #[derive(Debug, Serialize, Deserialize)] 52 | struct BenchmarkResult { 53 | operation: String, 54 | operand_type: String, 55 | iterations: usize, 56 | duration_ns: u128, 57 | throughput: f64, // Operations per second 58 | } 59 | 60 | #[derive(Debug, Serialize, Deserialize)] 61 | struct BenchmarkSuite { 62 | timestamp: String, 63 | system_info: SystemInfo, 64 | results: Vec, 65 | } 66 | 67 | #[derive(Debug, Serialize, Deserialize)] 68 | struct SystemInfo { 69 | cpu_info: String, 70 | memory_mb: u64, 71 | os: String, 72 | rust_version: String, 73 | } 74 | 75 | // Generate random i24 values within the valid range 76 | fn generate_i24_values(count: usize, seed: u64) -> Vec { 77 | let mut rng = StdRng::seed_from_u64(seed); 78 | let range = i24::MIN.to_i32()..=i24::MAX.to_i32(); 79 | 80 | (0..count) 81 | .map(|_| { 82 | let value = rng.gen_range(range.clone()); 83 | i24::try_from_i32(value).unwrap_or(i24!(0)) 84 | }) 85 | .collect() 86 | } 87 | 88 | // Generate random i32 values for comparison 89 | fn generate_i32_values(count: usize, seed: u64) -> Vec { 90 | let mut rng = StdRng::seed_from_u64(seed); 91 | // Use the same range as i24 for fairness 92 | let range = i24::MIN.to_i32()..=i24::MAX.to_i32(); 93 | 94 | (0..count) 95 | .map(|_| rng.gen_range(range.clone())) 96 | .collect() 97 | } 98 | 99 | // Generate random shift amounts (u32) for shift operations 100 | fn generate_shift_values(count: usize, seed: u64) -> Vec { 101 | let mut rng = StdRng::seed_from_u64(seed); 102 | let range = 0..24; // Limit shifts to i24::BITS 103 | 104 | (0..count) 105 | .map(|_| rng.gen_range(range.clone())) 106 | .collect() 107 | } 108 | 109 | // Benchmark a binary operation on i24 values 110 | fn bench_binary_op(op: Operation, values: &[i24], op_func: F, iterations: usize) -> BenchmarkResult 111 | where 112 | F: Fn(i24, i24) -> i24 113 | { 114 | // Warmup 115 | let mut result = i24!(0); 116 | for i in 0..values.len()-1 { 117 | result = op_func(values[i], values[i+1]); 118 | } 119 | 120 | // Time it 121 | let start = Instant::now(); 122 | for _ in 0..iterations { 123 | for i in 0..values.len()-1 { 124 | result = op_func(values[i], values[i+1]); 125 | } 126 | } 127 | let duration = start.elapsed(); 128 | 129 | // Force the compiler to keep the result 130 | std::hint::black_box(result); 131 | 132 | let ops_count = (values.len() - 1) * iterations; 133 | let throughput = ops_count as f64 / (duration.as_secs_f64()); 134 | 135 | BenchmarkResult { 136 | operation: op.to_string(), 137 | operand_type: "i24".to_string(), 138 | iterations, 139 | duration_ns: duration.as_nanos(), 140 | throughput, 141 | } 142 | } 143 | 144 | // Benchmark a binary operation on i32 values for comparison 145 | fn bench_binary_op_i32(op: Operation, values: &[i32], op_func: F, iterations: usize) -> BenchmarkResult 146 | where 147 | F: Fn(i32, i32) -> i32 148 | { 149 | // Warmup 150 | let mut result = 0i32; 151 | for i in 0..values.len()-1 { 152 | result = op_func(values[i], values[i+1]); 153 | } 154 | 155 | // Time it 156 | let start = Instant::now(); 157 | for _ in 0..iterations { 158 | for i in 0..values.len()-1 { 159 | result = op_func(values[i], values[i+1]); 160 | } 161 | } 162 | let duration = start.elapsed(); 163 | 164 | // Force the compiler to keep the result 165 | std::hint::black_box(result); 166 | 167 | let ops_count = (values.len() - 1) * iterations; 168 | let throughput = ops_count as f64 / (duration.as_secs_f64()); 169 | 170 | BenchmarkResult { 171 | operation: op.to_string(), 172 | operand_type: "i32".to_string(), 173 | iterations, 174 | duration_ns: duration.as_nanos(), 175 | throughput, 176 | } 177 | } 178 | 179 | // Benchmark a unary operation on i24 values 180 | fn bench_unary_op(op: Operation, values: &[i24], op_func: F, iterations: usize) -> BenchmarkResult 181 | where 182 | F: Fn(i24) -> i24 183 | { 184 | // Warmup 185 | let mut result = i24!(0); 186 | for val in values { 187 | result = op_func(*val); 188 | } 189 | 190 | // Time it 191 | let start = Instant::now(); 192 | for _ in 0..iterations { 193 | for val in values { 194 | result = op_func(*val); 195 | } 196 | } 197 | let duration = start.elapsed(); 198 | 199 | // Force the compiler to keep the results 200 | std::hint::black_box(result); 201 | 202 | let ops_count = values.len() * iterations; 203 | let throughput = ops_count as f64 / (duration.as_secs_f64()); 204 | 205 | BenchmarkResult { 206 | operation: op.to_string(), 207 | operand_type: "i24".to_string(), 208 | iterations, 209 | duration_ns: duration.as_nanos(), 210 | throughput, 211 | } 212 | } 213 | 214 | // Benchmark a unary operation on i32 values for comparison 215 | fn bench_unary_op_i32(op: Operation, values: &[i32], op_func: F, iterations: usize) -> BenchmarkResult 216 | where 217 | F: Fn(i32) -> i32 218 | { 219 | // Warmup 220 | let mut result = 0i32; 221 | for val in values { 222 | result = op_func(*val); 223 | } 224 | 225 | // Time it 226 | let start = Instant::now(); 227 | for _ in 0..iterations { 228 | for val in values { 229 | result = op_func(*val); 230 | } 231 | } 232 | let duration = start.elapsed(); 233 | 234 | // Force the compiler to keep the results 235 | std::hint::black_box(result); 236 | 237 | let ops_count = values.len() * iterations; 238 | let throughput = ops_count as f64 / (duration.as_secs_f64()); 239 | 240 | BenchmarkResult { 241 | operation: op.to_string(), 242 | operand_type: "i32".to_string(), 243 | iterations, 244 | duration_ns: duration.as_nanos(), 245 | throughput, 246 | } 247 | } 248 | 249 | // Benchmark a shift operation on i24 values 250 | fn bench_shift_op(op: Operation, values: &[i24], shift_amounts: &[u32], op_func: F, iterations: usize) -> BenchmarkResult 251 | where 252 | F: Fn(i24, u32) -> i24 253 | { 254 | // Warmup 255 | let mut result = i24!(0); 256 | for i in 0..values.len() { 257 | let shift = shift_amounts[i % shift_amounts.len()]; 258 | result = op_func(values[i], shift); 259 | } 260 | 261 | // Time it 262 | let start = Instant::now(); 263 | for _ in 0..iterations { 264 | for i in 0..values.len() { 265 | let shift = shift_amounts[i % shift_amounts.len()]; 266 | result = op_func(values[i], shift); 267 | } 268 | } 269 | let duration = start.elapsed(); 270 | 271 | // Force the compiler to keep the result 272 | std::hint::black_box(result); 273 | 274 | let ops_count = values.len() * iterations; 275 | let throughput = ops_count as f64 / (duration.as_secs_f64()); 276 | 277 | BenchmarkResult { 278 | operation: op.to_string(), 279 | operand_type: "i24".to_string(), 280 | iterations, 281 | duration_ns: duration.as_nanos(), 282 | throughput, 283 | } 284 | } 285 | 286 | // Benchmark a shift operation on i32 values for comparison 287 | fn bench_shift_op_i32(op: Operation, values: &[i32], shift_amounts: &[u32], op_func: F, iterations: usize) -> BenchmarkResult 288 | where 289 | F: Fn(i32, u32) -> i32 290 | { 291 | // Warmup 292 | let mut result = 0i32; 293 | for i in 0..values.len() { 294 | let shift = shift_amounts[i % shift_amounts.len()]; 295 | result = op_func(values[i], shift); 296 | } 297 | 298 | // Time it 299 | let start = Instant::now(); 300 | for _ in 0..iterations { 301 | for i in 0..values.len() { 302 | let shift = shift_amounts[i % shift_amounts.len()]; 303 | result = op_func(values[i], shift); 304 | } 305 | } 306 | let duration = start.elapsed(); 307 | 308 | // Force the compiler to keep the result 309 | std::hint::black_box(result); 310 | 311 | let ops_count = values.len() * iterations; 312 | let throughput = ops_count as f64 / (duration.as_secs_f64()); 313 | 314 | BenchmarkResult { 315 | operation: op.to_string(), 316 | operand_type: "i32".to_string(), 317 | iterations, 318 | duration_ns: duration.as_nanos(), 319 | throughput, 320 | } 321 | } 322 | 323 | // Benchmark conversions 324 | fn bench_i24_to_i32(values: &[i24], iterations: usize) -> BenchmarkResult { 325 | // Warmup 326 | let mut result = 0i32; 327 | for val in values { 328 | result = val.to_i32(); 329 | } 330 | 331 | // Time it 332 | let start = Instant::now(); 333 | for _ in 0..iterations { 334 | for val in values { 335 | result = val.to_i32(); 336 | } 337 | } 338 | let duration = start.elapsed(); 339 | 340 | // Force the compiler to keep the result 341 | std::hint::black_box(result); 342 | 343 | let ops_count = values.len() * iterations; 344 | let throughput = ops_count as f64 / (duration.as_secs_f64()); 345 | 346 | BenchmarkResult { 347 | operation: Operation::ToI32.to_string(), 348 | operand_type: "i24".to_string(), 349 | iterations, 350 | duration_ns: duration.as_nanos(), 351 | throughput, 352 | } 353 | } 354 | 355 | fn bench_i32_to_i24(values: &[i32], iterations: usize) -> BenchmarkResult { 356 | // Warmup 357 | let mut result = i24!(0); 358 | for val in values { 359 | result = i24::wrapping_from_i32(*val); 360 | } 361 | 362 | // Time it 363 | let start = Instant::now(); 364 | for _ in 0..iterations { 365 | for val in values { 366 | result = i24::wrapping_from_i32(*val); 367 | } 368 | } 369 | let duration = start.elapsed(); 370 | 371 | // Force the compiler to keep the result 372 | std::hint::black_box(result); 373 | 374 | let ops_count = values.len() * iterations; 375 | let throughput = ops_count as f64 / (duration.as_secs_f64()); 376 | 377 | BenchmarkResult { 378 | operation: Operation::FromI32.to_string(), 379 | operand_type: "i24".to_string(), 380 | iterations, 381 | duration_ns: duration.as_nanos(), 382 | throughput, 383 | } 384 | } 385 | 386 | // Benchmark byte conversion operations 387 | fn bench_i24_byte_conversions(values: &[i24], iterations: usize) -> BenchmarkResult { 388 | // Warmup 389 | let mut bytes = [0u8; 3]; 390 | for val in values { 391 | bytes = val.to_le_bytes(); 392 | let _ = i24::from_le_bytes(bytes); 393 | } 394 | 395 | // Time it 396 | let start = Instant::now(); 397 | for _ in 0..iterations { 398 | for val in values { 399 | bytes = val.to_le_bytes(); 400 | let _ = i24::from_le_bytes(bytes); 401 | } 402 | } 403 | let duration = start.elapsed(); 404 | 405 | let ops_count = values.len() * iterations; 406 | let throughput = ops_count as f64 / (duration.as_secs_f64()); 407 | 408 | BenchmarkResult { 409 | operation: Operation::ByteConversion.to_string(), 410 | operand_type: "i24".to_string(), 411 | iterations, 412 | duration_ns: duration.as_nanos(), 413 | throughput, 414 | } 415 | } 416 | 417 | // Run all benchmarks and return results 418 | fn run_benchmarks(sample_size: usize, iterations: usize) -> BenchmarkSuite { 419 | // Generate test data 420 | let seed = 42; // Fixed seed for reproducibility 421 | let i24_values = generate_i24_values(sample_size, seed); 422 | let i32_values = generate_i32_values(sample_size, seed); 423 | let shift_values = generate_shift_values(sample_size, seed); 424 | 425 | let mut results = Vec::new(); 426 | 427 | // Binary operations 428 | println!("Benchmarking binary operations..."); 429 | 430 | // Addition 431 | results.push(bench_binary_op( 432 | Operation::Add, 433 | &i24_values, 434 | |a, b| a + b, 435 | iterations 436 | )); 437 | 438 | 439 | 440 | results.push(bench_binary_op_i32( 441 | Operation::Add, 442 | &i32_values, 443 | |a, b| a + b, 444 | iterations 445 | )); 446 | 447 | // Subtraction 448 | results.push(bench_binary_op( 449 | Operation::Sub, 450 | &i24_values, 451 | |a, b| a - b, 452 | iterations 453 | )); 454 | results.push(bench_binary_op_i32( 455 | Operation::Sub, 456 | &i32_values, 457 | |a, b| a - b, 458 | iterations 459 | )); 460 | 461 | // Multiplication 462 | results.push(bench_binary_op( 463 | Operation::Mul, 464 | &i24_values, 465 | |a, b| a * b, 466 | iterations 467 | )); 468 | results.push(bench_binary_op_i32( 469 | Operation::Mul, 470 | &i32_values, 471 | |a, b| a * b, 472 | iterations 473 | )); 474 | 475 | // Division 476 | // Filter out zeros to avoid division by zero 477 | let div_i24_values: Vec = i24_values.iter().filter(|&x| *x != i24!(0)).cloned().collect(); 478 | let div_i32_values: Vec = i32_values.iter().filter(|&x| *x != 0).cloned().collect(); 479 | 480 | results.push(bench_binary_op( 481 | Operation::Div, 482 | &div_i24_values, 483 | |a, b| a / b, 484 | iterations 485 | )); 486 | results.push(bench_binary_op_i32( 487 | Operation::Div, 488 | &div_i32_values, 489 | |a, b| a / b, 490 | iterations 491 | )); 492 | 493 | // Remainder 494 | results.push(bench_binary_op( 495 | Operation::Rem, 496 | &div_i24_values, 497 | |a, b| a % b, 498 | iterations 499 | )); 500 | results.push(bench_binary_op_i32( 501 | Operation::Rem, 502 | &div_i32_values, 503 | |a, b| a % b, 504 | iterations 505 | )); 506 | 507 | // Bitwise operations 508 | results.push(bench_binary_op( 509 | Operation::BitAnd, 510 | &i24_values, 511 | |a, b| a & b, 512 | iterations 513 | )); 514 | results.push(bench_binary_op_i32( 515 | Operation::BitAnd, 516 | &i32_values, 517 | |a, b| a & b, 518 | iterations 519 | )); 520 | 521 | results.push(bench_binary_op( 522 | Operation::BitOr, 523 | &i24_values, 524 | |a, b| a | b, 525 | iterations 526 | )); 527 | results.push(bench_binary_op_i32( 528 | Operation::BitOr, 529 | &i32_values, 530 | |a, b| a | b, 531 | iterations 532 | )); 533 | 534 | results.push(bench_binary_op( 535 | Operation::BitXor, 536 | &i24_values, 537 | |a, b| a ^ b, 538 | iterations 539 | )); 540 | results.push(bench_binary_op_i32( 541 | Operation::BitXor, 542 | &i32_values, 543 | |a, b| a ^ b, 544 | iterations 545 | )); 546 | 547 | // Shift operations 548 | println!("Benchmarking shift operations..."); 549 | results.push(bench_shift_op( 550 | Operation::Shl, 551 | &i24_values, 552 | &shift_values, 553 | |a, s| a << s, 554 | iterations 555 | )); 556 | results.push(bench_shift_op_i32( 557 | Operation::Shl, 558 | &i32_values, 559 | &shift_values, 560 | |a, s| a << s, 561 | iterations 562 | )); 563 | 564 | results.push(bench_shift_op( 565 | Operation::Shr, 566 | &i24_values, 567 | &shift_values, 568 | |a, s| a >> s, 569 | iterations 570 | )); 571 | results.push(bench_shift_op_i32( 572 | Operation::Shr, 573 | &i32_values, 574 | &shift_values, 575 | |a, s| a >> s, 576 | iterations 577 | )); 578 | 579 | // Unary operations 580 | println!("Benchmarking unary operations..."); 581 | results.push(bench_unary_op( 582 | Operation::Neg, 583 | &i24_values, 584 | |a| -a, 585 | iterations 586 | )); 587 | results.push(bench_unary_op_i32( 588 | Operation::Neg, 589 | &i32_values, 590 | |a| -a, 591 | iterations 592 | )); 593 | 594 | results.push(bench_unary_op( 595 | Operation::Not, 596 | &i24_values, 597 | |a| !a, 598 | iterations 599 | )); 600 | results.push(bench_unary_op_i32( 601 | Operation::Not, 602 | &i32_values, 603 | |a| !a, 604 | iterations 605 | )); 606 | 607 | // Conversion operations 608 | println!("Benchmarking conversions..."); 609 | results.push(bench_i24_to_i32(&i24_values, iterations)); 610 | results.push(bench_i32_to_i24(&i32_values, iterations)); 611 | 612 | // Byte conversion operations 613 | println!("Benchmarking byte conversions..."); 614 | results.push(bench_i24_byte_conversions(&i24_values, iterations)); 615 | 616 | // Create benchmark suite with system info 617 | BenchmarkSuite { 618 | timestamp: chrono::Local::now().to_rfc3339(), 619 | system_info: get_system_info(), 620 | results, 621 | } 622 | } 623 | 624 | // Get basic system information 625 | fn get_system_info() -> SystemInfo { 626 | SystemInfo { 627 | cpu_info: sys_info::cpu_num().map(|n| format!("{} cores", n)).unwrap_or_else(|_| "Unknown".to_string()), 628 | memory_mb: sys_info::mem_info().map(|m| m.total / 1024).unwrap_or(0), 629 | os: format!("{} {}", 630 | sys_info::os_type().unwrap_or_else(|_| "Unknown".to_string()), 631 | sys_info::os_release().unwrap_or_else(|_| "Unknown".to_string())), 632 | rust_version: rustc_version_runtime::version().to_string(), 633 | } 634 | } 635 | 636 | fn main() -> Result<(), Box> { 637 | // Configure benchmark parameters 638 | let sample_size = 10_000_000; 639 | let iterations = 1_000; 640 | 641 | println!("Starting i24 benchmarks"); 642 | println!("Sample size: {}, Iterations: {}", sample_size, iterations); 643 | 644 | // Run benchmarks 645 | let benchmark_suite = run_benchmarks(sample_size, iterations); 646 | 647 | // Write results to JSON file 648 | let file = File::create("i24_benchmark_results.json")?; 649 | serde_json::to_writer_pretty(file, &benchmark_suite)?; 650 | 651 | println!("Benchmarks complete. Results written to i24_benchmark_results.json"); 652 | 653 | // Print summary to console 654 | println!("\nSummary:"); 655 | println!("{:<15} {:<10} {:<15} {:<15}", "Operation", "Type", "Duration (ms)", "Ops/sec"); 656 | println!("{}", "-".repeat(60)); 657 | 658 | for result in &benchmark_suite.results { 659 | println!("{:<15} {:<10} {:<15.2} {:<15.0}", 660 | result.operation, 661 | result.operand_type, 662 | result.duration_ns as f64 / 1_000_000.0, 663 | result.throughput); 664 | } 665 | 666 | Ok(()) 667 | } -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmg049/i24/4d0a7483ec2fb72ccec9524c5b3b1d8a63a44b2e/logo.png -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | //! # i24: A 24-bit Signed Integer Type 4 | //! 5 | //! The `i24` crate provides a 24-bit signed integer type for Rust, filling the gap between 6 | //! `i16` and `i32`. This type is particularly useful in audio processing, certain embedded 7 | //! systems, and other scenarios where 24-bit precision is required but 32 bits would be excessive. 8 | //! 9 | //! ## Features 10 | //! 11 | //! - Efficient 24-bit signed integer representation 12 | //! - Seamless conversion to and from `i32` 13 | //! - Support for basic arithmetic operations with overflow checking 14 | //! - Bitwise operations 15 | //! - Conversions from various byte representations (little-endian, big-endian, native) 16 | //! - Implements common traits like `Debug`, `Display`, `PartialEq`, `Eq`, `PartialOrd`, `Ord`, and `Hash` 17 | //! 18 | //! This crate came about as a part of the [Wavers](https://crates.io/crates/wavers) project, which is a Wav file reader and writer for Rust. 19 | //! The `i24` struct also has pyo3 bindings for use in Python. Enable the ``pyo3`` feature to use the pyo3 bindings. 20 | //! 21 | //! ## Usage 22 | //! 23 | //! Add this to your `Cargo.toml`: 24 | //! 25 | //! ```toml 26 | //! [dependencies] 27 | //! i24 = "2.1.0" 28 | //! ``` 29 | //! 30 | //! Then, in your Rust code: 31 | //! 32 | //! ```rust 33 | //! # #[macro_use] extern crate i24; 34 | //! 35 | //! let a = i24!(1000); 36 | //! let b = i24!(2000); 37 | //! let c = a + b; 38 | //! assert_eq!(c.to_i32(), 3000); 39 | //! assert_eq!(c, i24!(3000)); 40 | //! ``` 41 | //! The `i24!` macro allows you to create `i24` values at compile time, ensuring that the value is within the valid range. 42 | //! 43 | //! Then if working with 3-byte representations from disk or the network, you can use the `I24DiskMethods` trait to read and write `i24` slices of `i24`. 44 | //! 45 | //! ```ignore 46 | //! use i24::I24DiskMethods; // Bring extension trait into scope 47 | //! use i24::i24 as I24; // Import the i24 type 48 | //! let raw_data: &[u8] = &[0x00, 0x01, 0x02, 0x00, 0x01, 0xFF]; // 2 values 49 | //! let values: Vec = I24::read_i24s_be(raw_data).expect("valid buffer"); 50 | //! 51 | //! let encoded: Vec = I24::write_i24s_be(&values); 52 | //! assert_eq!(encoded, raw_data); 53 | //! ``` 54 | //! 55 | //! ## Safety and Limitations 56 | //! 57 | //! While `i24` strives to behave similarly to Rust's built-in integer types, there are some 58 | //! important considerations: 59 | //! 60 | //! - The valid range for `i24` is [-8,388,608, 8,388,607]. 61 | //! - Overflow behavior in arithmetic operations matches that of `i32`. 62 | //! - Bitwise operations are performed on the 24-bit representation. 63 | //! 64 | //! Always use checked arithmetic operations when dealing with untrusted input or when 65 | //! overflow/underflow is a concern. 66 | //! 67 | //! 'i24' aligns with the safety requirements of bytemuck (NoUninit, Zeroable and bytemuck::AnyBitPattern), ensuring that it is safe to use for converting between valid bytes and a i24 value. 68 | //! Then when using the `I24DiskMethods` trait, it is safe to use (internally) the `bytemuck::cast_slice` function to convert between a slice of bytes and a slice of 'i24' values. 69 | //! 70 | //! ## Features 71 | //! - **pyo3**: Enables the pyo3 bindings for the `i24` type. 72 | //! - **serde**: Enables the `Serialize` and `Deserialize` traits for the `i24` type. 73 | //! - **alloc**: Enables the `I24DiskMethods` trait for the `i24` type. 74 | //! 75 | //! ## Contributing 76 | //! 77 | //! Contributions are welcome! Please feel free to submit a Pull Request. This really needs more testing and verification. 78 | //! 79 | //! ## License 80 | //! 81 | //! This project is licensed under MIT - see the [LICENSE](https://github.com/jmg049/i24/blob/main/LICENSE) file for details. 82 | //! 83 | //! ## Benchmarks 84 | //! See the [benchmark report](https://github.com/jmg049/i24/i24_benches/benchmark_analysis/benchmark_report.md). 85 | //! 86 | 87 | use crate::repr::I24Repr; 88 | use bytemuck::{NoUninit, Zeroable}; 89 | 90 | #[cfg(feature = "alloc")] 91 | use repr::DiskI24; 92 | 93 | #[cfg(feature = "alloc")] 94 | use bytemuck::cast_slice; 95 | 96 | use core::fmt; 97 | use core::fmt::{Debug, Display, LowerHex, Octal, UpperHex}; 98 | use core::hash::{Hash, Hasher}; 99 | use core::num::ParseIntError; 100 | use core::ops::{ 101 | Add, AddAssign, BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Div, DivAssign, 102 | Mul, MulAssign, Rem, RemAssign, Shl, ShlAssign, Shr, ShrAssign, Sub, SubAssign, 103 | }; 104 | use core::{ 105 | ops::{Neg, Not}, 106 | str::FromStr, 107 | }; 108 | use num_traits::{Num, One, ToBytes, Zero}; 109 | 110 | #[cfg(feature = "pyo3")] 111 | use pyo3::prelude::*; 112 | 113 | #[cfg(feature = "pyo3")] 114 | use numpy::PyArrayDescr; 115 | 116 | #[cfg(feature = "std")] 117 | extern crate std; 118 | 119 | mod repr; 120 | 121 | #[allow(non_camel_case_types)] 122 | #[repr(transparent)] 123 | #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)] 124 | #[cfg_attr(feature = "pyo3", pyclass)] 125 | /// A signed 24-bit integer type. 126 | /// 127 | /// The `i24` type represents a 24-bit signed integer in two's complement format. It fills the gap between 128 | /// `i16` and `i32`, offering reduced memory usage while preserving a larger numeric range than `i16`. 129 | /// 130 | /// This type is particularly suited to applications such as audio processing, binary file manipulation, 131 | /// embedded systems, or networking protocols where 24-bit integers are common. 132 | /// 133 | /// ## Range 134 | /// 135 | /// The valid value range is: 136 | /// 137 | /// ```text 138 | /// MIN = -8_388_608 // -2^23 139 | /// MAX = 8_388_607 // 2^23 - 1 140 | /// ``` 141 | /// 142 | /// Arithmetic operations are implemented to match Rust’s standard integer behavior, 143 | /// including overflow and checked variants. 144 | /// 145 | /// ## Memory Layout and Safety 146 | /// 147 | /// `i24` is implemented as a `#[repr(transparent)]` wrapper around a 4-byte internal representation. 148 | /// Although the logical width is 24 bits, one additional byte is used for alignment and padding control. 149 | /// 150 | /// This struct: 151 | /// 152 | /// - Contains **no uninitialized or padding bytes** 153 | /// - Is **safe to use** with [`bytemuck::NoUninit`](https://docs.rs/bytemuck/latest/bytemuck/trait.NoUninit.html) 154 | /// - Can be cast safely with [`bytemuck::cast_slice`] for use in FFI and low-level applications 155 | /// 156 | /// The layout is verified via compile-time assertions to ensure portability and correctness. 157 | /// 158 | /// ## Binary Serialization 159 | /// 160 | /// Although `i24` occupies 4 bytes in memory, binary formats (e.g., WAV files, network protocols) often 161 | /// store 24-bit integers in a 3-byte representation. To support this: 162 | /// 163 | /// - `i24` provides [`from_be_bytes`], [`from_le_bytes`], and [`from_ne_bytes`] methods for constructing 164 | /// values from 3-byte on-disk representations 165 | /// - Corresponding [`to_be_bytes`], [`to_le_bytes`], and [`to_ne_bytes`] methods convert to 3-byte representations 166 | /// 167 | /// For efficient bulk deserialization, use the [`I24DiskMethods`] extension trait. 168 | /// 169 | /// Note: Requires the ``alloc`` feature to be enabled. 170 | /// 171 | /// ```ignore 172 | /// use i24::I24DiskMethods; 173 | /// use i24::i24 as I24; 174 | /// let raw: &[u8] = &[0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF]; 175 | /// let values = I24::read_i24s_be(raw).unwrap(); 176 | /// assert_eq!(values[0].to_i32(), 1); 177 | /// assert_eq!(values[1].to_i32(), -1); 178 | /// ``` 179 | /// 180 | /// ## Usage Notes 181 | /// 182 | /// - Use the `i24!` macro for compile-time checked construction 183 | /// - Use `.to_i32()` to convert to standard Rust types 184 | /// - Supports traits like `Add`, `Sub`, `Display`, `Hash`, `Ord`, and `FromStr` 185 | /// 186 | /// ## Features 187 | /// 188 | /// - **`serde`**: Enables `Serialize` and `Deserialize` support via JSON or other formats 189 | /// - **`pyo3`**: Exposes the `i24` type to Python via PyO3 bindings (as `I24`) 190 | /// - **`alloc`**: Enables `I24DiskMethods` for efficient bulk serialization/deserialization 191 | pub struct i24(I24Repr); 192 | 193 | // Safety: repr(transparent) and so if I24Repr is Zeroable so should i24 be 194 | unsafe impl Zeroable for i24 where I24Repr: Zeroable {} 195 | 196 | // Safety: repr(transparent) and so if I24Repr is NoUninit so should i24 be 197 | // Must be NoUninit due to the padding byte. 198 | unsafe impl NoUninit for i24 where I24Repr: NoUninit {} 199 | 200 | #[doc(hidden)] 201 | pub mod __macros__ { 202 | pub use bytemuck::Zeroable; 203 | } 204 | 205 | /// creates an `i24` from a constant expression 206 | /// will give a compile error if the expression overflows an i24 207 | #[macro_export] 208 | macro_rules! i24 { 209 | (0) => { 210 | ::zeroed() 211 | }; 212 | ($e: expr) => { 213 | const { 214 | match $crate::i24::try_from_i32($e) { 215 | Some(x) => x, 216 | None => panic!(concat!( 217 | "out of range value ", 218 | stringify!($e), 219 | " used as an i24 constant" 220 | )), 221 | } 222 | } 223 | }; 224 | } 225 | 226 | impl i24 { 227 | /// The size of this integer type in bits 228 | pub const BITS: u32 = 24; 229 | 230 | /// The smallest value that can be represented by this integer type (-223) 231 | pub const MIN: i24 = i24!(I24Repr::MIN); 232 | 233 | /// The largest value that can be represented by this integer type (223 − 1). 234 | pub const MAX: i24 = i24!(I24Repr::MAX); 235 | 236 | #[inline(always)] 237 | const fn as_bits(&self) -> &u32 { 238 | self.0.as_bits() 239 | } 240 | 241 | #[inline(always)] 242 | const fn to_bits(self) -> u32 { 243 | self.0.to_bits() 244 | } 245 | 246 | /// Safety: see `I24Repr::from_bits` 247 | #[inline(always)] 248 | const unsafe fn from_bits(bits: u32) -> i24 { 249 | Self(unsafe { I24Repr::from_bits(bits) }) 250 | } 251 | 252 | /// same as `Self::from_bits` but always truncates 253 | #[inline(always)] 254 | const fn from_bits_truncate(bits: u32) -> i24 { 255 | // the most significant byte is zeroed out 256 | Self(unsafe { I24Repr::from_bits(bits & I24Repr::BITS_MASK) }) 257 | } 258 | 259 | /// Converts the 24-bit integer to a 32-bit signed integer. 260 | /// 261 | /// This method performs sign extension if the 24-bit integer is negative. 262 | /// 263 | /// # Returns 264 | /// 265 | /// The 32-bit signed integer representation of this `i24`. 266 | #[inline(always)] 267 | pub const fn to_i32(self) -> i32 { 268 | self.0.to_i32() 269 | } 270 | 271 | /// Creates an `i24` from a 32-bit signed integer. 272 | /// 273 | /// This method truncates the input to 24 bits if it's outside the valid range. 274 | /// 275 | /// # Arguments 276 | /// 277 | /// * `n` - The 32-bit signed integer to convert. 278 | /// 279 | /// # Returns 280 | /// 281 | /// An `i24` instance representing the input value. 282 | #[inline(always)] 283 | pub const fn wrapping_from_i32(n: i32) -> Self { 284 | Self(I24Repr::wrapping_from_i32(n)) 285 | } 286 | 287 | /// Creates an `i24` from a 32-bit signed integer. 288 | /// 289 | /// This method saturates the input if it's outside the valid range. 290 | /// 291 | /// # Arguments 292 | /// 293 | /// * `n` - The 32-bit signed integer to convert. 294 | /// 295 | /// # Returns 296 | /// 297 | /// An `i24` instance representing the input value. 298 | #[inline(always)] 299 | pub const fn saturating_from_i32(n: i32) -> Self { 300 | Self(I24Repr::saturating_from_i32(n)) 301 | } 302 | 303 | /// Reverses the byte order of the integer. 304 | #[inline(always)] 305 | pub const fn swap_bytes(self) -> Self { 306 | Self(self.0.swap_bytes()) 307 | } 308 | 309 | /// Converts self to little endian from the target's endianness. 310 | /// On little endian this is a no-op. On big endian the bytes are swapped. 311 | #[inline(always)] 312 | pub const fn to_le(self) -> Self { 313 | Self(self.0.to_le()) 314 | } 315 | 316 | /// Converts self to big endian from the target's endianness. 317 | /// On big endian this is a no-op. On little endian the bytes are swapped. 318 | #[inline(always)] 319 | pub const fn to_be(self) -> Self { 320 | Self(self.0.to_be()) 321 | } 322 | 323 | /// Return the memory representation of this integer as a byte array in native byte order. 324 | /// As the target platform's native endianness is used, 325 | /// portable code should use to_be_bytes or to_le_bytes, as appropriate, instead. 326 | #[inline(always)] 327 | pub const fn to_ne_bytes(self) -> [u8; 3] { 328 | self.0.to_ne_bytes() 329 | } 330 | 331 | /// Create a native endian integer value from its representation as a byte array in little endian. 332 | #[inline(always)] 333 | pub const fn to_le_bytes(self) -> [u8; 3] { 334 | self.0.to_le_bytes() 335 | } 336 | 337 | /// Return the memory representation of this integer as a byte array in big-endian (network) byte order. 338 | #[inline(always)] 339 | pub const fn to_be_bytes(self) -> [u8; 3] { 340 | self.0.to_be_bytes() 341 | } 342 | 343 | /// Creates an `i24` from three bytes in **native endian** order. 344 | /// 345 | /// # Arguments 346 | /// 347 | /// * `bytes` - An array of 3 bytes representing the 24-bit integer. 348 | /// 349 | /// # Returns 350 | /// 351 | /// An `i24` instance containing the input bytes. 352 | #[inline(always)] 353 | pub const fn from_ne_bytes(bytes: [u8; 3]) -> Self { 354 | Self(I24Repr::from_ne_bytes(bytes)) 355 | } 356 | 357 | /// Creates an `i24` from three bytes in **little-endian** order. 358 | /// 359 | /// # Arguments 360 | /// 361 | /// * `bytes` - An array of 3 bytes representing the 24-bit integer in little-endian order. 362 | /// 363 | /// # Returns 364 | /// 365 | /// An `i24` instance containing the input bytes. 366 | #[inline(always)] 367 | pub const fn from_le_bytes(bytes: [u8; 3]) -> Self { 368 | Self(I24Repr::from_le_bytes(bytes)) 369 | } 370 | 371 | /// Creates an `i24` from three bytes in **big-endian** order. 372 | /// 373 | /// # Arguments 374 | /// 375 | /// * `bytes` - An array of 3 bytes representing the 24-bit integer in big-endian order. 376 | /// 377 | /// # Returns 378 | /// 379 | /// An `i24` instance with the bytes in little-endian order. 380 | #[inline(always)] 381 | pub const fn from_be_bytes(bytes: [u8; 3]) -> Self { 382 | Self(I24Repr::from_be_bytes(bytes)) 383 | } 384 | 385 | /// Performs checked addition. 386 | /// 387 | /// # Arguments 388 | /// 389 | /// * `other` - The `i24` to add to this value. 390 | /// 391 | /// # Returns 392 | /// 393 | /// `Some(i24)` if the addition was successful, or `None` if it would overflow. 394 | pub fn checked_add(self, other: Self) -> Option { 395 | self.to_i32() 396 | .checked_add(other.to_i32()) 397 | .and_then(Self::try_from_i32) 398 | } 399 | 400 | /// Performs checked subtraction. 401 | /// 402 | /// # Arguments 403 | /// 404 | /// * `other` - The `i24` to subtract from this value. 405 | /// 406 | /// # Returns 407 | /// 408 | /// `Some(i24)` if the subtraction was successful, or `None` if it would overflow. 409 | pub fn checked_sub(self, other: Self) -> Option { 410 | self.to_i32() 411 | .checked_sub(other.to_i32()) 412 | .and_then(Self::try_from_i32) 413 | } 414 | 415 | /// Performs checked multiplication. 416 | /// 417 | /// # Arguments 418 | /// 419 | /// * `other` - The `i24` to multiply with this value. 420 | /// 421 | /// # Returns 422 | /// 423 | /// `Some(i24)` if the multiplication was successful, or `None` if it would overflow. 424 | pub fn checked_mul(self, other: Self) -> Option { 425 | self.to_i32() 426 | .checked_mul(other.to_i32()) 427 | .and_then(Self::try_from_i32) 428 | } 429 | 430 | /// Performs checked division. 431 | /// 432 | /// # Arguments 433 | /// 434 | /// * `other` - The `i24` to divide this value by. 435 | /// 436 | /// # Returns 437 | /// 438 | /// `Some(i24)` if the division was successful, or `None` if the divisor is zero or if the division would overflow. 439 | pub fn checked_div(self, other: Self) -> Option { 440 | self.to_i32() 441 | .checked_div(other.to_i32()) 442 | .and_then(Self::try_from_i32) 443 | } 444 | 445 | /// Performs checked integer remainder. 446 | /// 447 | /// # Arguments 448 | /// 449 | /// * `other` - The `i24` to divide this value by. 450 | /// 451 | /// # Returns 452 | /// 453 | /// `Some(i24)` if the remainder operation was successful, or `None` if the divisor is zero or if the division would overflow. 454 | pub fn checked_rem(self, other: Self) -> Option { 455 | self.to_i32() 456 | .checked_rem(other.to_i32()) 457 | .and_then(Self::try_from_i32) 458 | } 459 | } 460 | 461 | #[cfg(feature = "alloc")] 462 | extern crate alloc; 463 | 464 | #[cfg(feature = "alloc")] 465 | use alloc::vec::Vec; 466 | 467 | /// Extension trait for performing efficient binary (de)serialization of [`i24`] values. 468 | /// 469 | /// This trait provides methods to read and write slices of [`i24`] values to and from 470 | /// raw byte buffers using standard 3-byte representations in various byte orders. 471 | /// 472 | /// These methods are especially useful when working with binary formats like audio files, 473 | /// network protocols, or memory-mapped files that use 24-bit integer encodings. 474 | /// 475 | /// The methods support safe and zero-copy deserialization using [`bytemuck::cast_slice`], 476 | /// under the guarantee that the on-disk format is valid and byte-aligned. 477 | /// 478 | /// ## Usage 479 | /// 480 | /// This trait is not implemented on `[u8]` or `[i24]` directly. 481 | /// Instead, you must import the trait and call methods via the [`i24`] type: 482 | /// 483 | /// ```rust 484 | /// use i24::I24DiskMethods; // Bring extension trait into scope 485 | /// use i24::i24 as I24; // Import the i24 type 486 | /// let raw_data: &[u8] = &[0x00, 0x01, 0x02, 0x00, 0x01, 0xFF]; // 2 values 487 | /// let values: Vec = I24::read_i24s_be(raw_data).expect("valid buffer"); 488 | /// 489 | /// let encoded: Vec = I24::write_i24s_be(&values); 490 | /// assert_eq!(encoded, raw_data); 491 | /// ``` 492 | /// 493 | /// ### Byte Order 494 | /// 495 | /// Each method clearly indicates the expected byte order: 496 | /// 497 | /// - `*_be`: Big-endian (most significant byte first) 498 | /// - `*_le`: Little-endian 499 | /// - `*_ne`: Native-endian (depends on target platform) 500 | /// 501 | /// ## Safety 502 | /// 503 | /// The `*_unchecked` variants skip input validation and assume: 504 | /// 505 | /// - The input slice length is a multiple of 3 506 | /// - The slice is properly aligned and valid for casting to a [`crate::DiskI24`] 507 | /// 508 | #[cfg(feature = "alloc")] 509 | pub trait I24DiskMethods { 510 | fn read_i24s_be(bytes: &[u8]) -> Option> { 511 | if bytes.len() % 3 != 0 { 512 | return None; 513 | } 514 | 515 | let chunks: &[DiskI24] = cast_slice(bytes); // Safe: NoUninit, packed, 3-byte chunks 516 | Some(chunks.iter().map(|b| i24::from_be_bytes(b.bytes)).collect()) 517 | } 518 | 519 | unsafe fn read_i24s_be_unchecked(bytes: &[u8]) -> Vec { 520 | let chunks: &[DiskI24] = cast_slice(bytes); // Safe: NoUninit, packed, 3-byte chunks 521 | chunks.iter().map(|b| i24::from_be_bytes(b.bytes)).collect() 522 | } 523 | 524 | fn write_i24s_be(values: &[i24]) -> Vec { 525 | values.iter().flat_map(|v| v.to_be_bytes()).collect() 526 | } 527 | 528 | fn read_i24s_le(bytes: &[u8]) -> Option> { 529 | if bytes.len() % 3 != 0 { 530 | return None; 531 | } 532 | 533 | let chunks: &[DiskI24] = cast_slice(bytes); // Safe: NoUninit, packed, 3-byte chunks 534 | Some(chunks.iter().map(|b| i24::from_le_bytes(b.bytes)).collect()) 535 | } 536 | 537 | unsafe fn read_i24s_le_unchecked(bytes: &[u8]) -> Vec { 538 | let chunks: &[DiskI24] = cast_slice(bytes); // Safe: NoUninit, packed, 3-byte chunks 539 | chunks.iter().map(|b| i24::from_le_bytes(b.bytes)).collect() 540 | } 541 | 542 | fn write_i24s_le(values: &[i24]) -> Vec { 543 | values.iter().flat_map(|v| v.to_le_bytes()).collect() 544 | } 545 | 546 | fn read_i24s_ne(bytes: &[u8]) -> Option> { 547 | if bytes.len() % 3 != 0 { 548 | return None; 549 | } 550 | 551 | let chunks: &[DiskI24] = cast_slice(bytes); // Safe: NoUninit, packed, 3-byte chunks 552 | Some(chunks.iter().map(|b| i24::from_ne_bytes(b.bytes)).collect()) 553 | } 554 | 555 | unsafe fn read_i24s_ne_unchecked(bytes: &[u8]) -> Vec { 556 | let chunks: &[DiskI24] = cast_slice(bytes); // Safe: NoUninit, packed, 3-byte chunks 557 | chunks.iter().map(|b| i24::from_ne_bytes(b.bytes)).collect() 558 | } 559 | 560 | fn write_i24s_ne(values: &[i24]) -> Vec { 561 | values.iter().flat_map(|v| v.to_ne_bytes()).collect() 562 | } 563 | } 564 | 565 | #[cfg(feature = "alloc")] 566 | impl I24DiskMethods for i24 {} 567 | 568 | type TryFromIntError = >::Error; 569 | 570 | fn out_of_range() -> TryFromIntError { 571 | i8::try_from(i64::MIN).unwrap_err() 572 | } 573 | 574 | macro_rules! impl_from { 575 | ($($ty: ty : $func_name: ident),+ $(,)?) => {$( 576 | impl From<$ty> for i24 { 577 | fn from(value: $ty) -> Self { 578 | Self::$func_name(value) 579 | } 580 | } 581 | 582 | impl i24 { 583 | pub const fn $func_name(value: $ty) -> Self { 584 | Self(I24Repr::$func_name(value)) 585 | } 586 | } 587 | )+}; 588 | } 589 | 590 | macro_rules! impl_try { 591 | ($($ty: ty : $func_name: ident),+ $(,)?) => {$( 592 | impl TryFrom<$ty> for i24 { 593 | type Error = TryFromIntError; 594 | 595 | fn try_from(value: $ty) -> Result { 596 | Self::$func_name(value).ok_or_else(out_of_range) 597 | } 598 | } 599 | 600 | impl i24 { 601 | pub const fn $func_name(value: $ty) -> Option { 602 | match I24Repr::$func_name(value) { 603 | Some(x) => Some(Self(x)), 604 | None => None 605 | } 606 | } 607 | } 608 | )+}; 609 | } 610 | 611 | impl_from! { 612 | u8: from_u8, 613 | u16: from_u16, 614 | bool: from_bool, 615 | 616 | i8: from_i8, 617 | i16: from_i16, 618 | } 619 | 620 | impl_try! { 621 | u32 : try_from_u32, 622 | u64 : try_from_u64, 623 | u128: try_from_u128, 624 | 625 | i32 : try_from_i32, 626 | i64 : try_from_i64, 627 | i128: try_from_i128, 628 | } 629 | 630 | impl One for i24 { 631 | fn one() -> Self { 632 | i24!(1) 633 | } 634 | } 635 | 636 | impl Zero for i24 { 637 | #[inline(always)] 638 | fn zero() -> Self { 639 | Self::zeroed() 640 | } 641 | 642 | #[inline(always)] 643 | fn is_zero(&self) -> bool { 644 | Self::zeroed() == *self 645 | } 646 | } 647 | 648 | pub const fn from_str_error(bad_val: &str) -> ParseIntError { 649 | match i8::from_str_radix(bad_val, 10) { 650 | Err(err) => err, 651 | Ok(_) => unreachable!(), 652 | } 653 | } 654 | 655 | pub const fn positive_overflow() -> ParseIntError { 656 | const { from_str_error("9999999999999999999999999999999999999999") } 657 | } 658 | 659 | pub const fn negative_overflow() -> ParseIntError { 660 | const { from_str_error("-9999999999999999999999999999999999999999") } 661 | } 662 | 663 | macro_rules! from_str { 664 | ($meth: ident($($args: tt)*)) => { 665 | i32::$meth($($args)*) 666 | .and_then(|x| i24::try_from_i32(x).ok_or_else(|| { 667 | if x < 0 { 668 | const { negative_overflow() } 669 | } else { 670 | const { positive_overflow() } 671 | } 672 | })) 673 | }; 674 | } 675 | 676 | impl Num for i24 { 677 | type FromStrRadixErr = ParseIntError; 678 | fn from_str_radix(str: &str, radix: u32) -> Result { 679 | from_str!(from_str_radix(str, radix)) 680 | } 681 | } 682 | 683 | impl FromStr for i24 { 684 | type Err = ParseIntError; 685 | 686 | fn from_str(str: &str) -> Result { 687 | from_str!(from_str(str)) 688 | } 689 | } 690 | 691 | macro_rules! impl_bin_op { 692 | ($(impl $op: ident = $assign: ident $assign_fn: ident { $($impl: tt)* })+) => {$( 693 | impl_bin_op!(impl $op = $assign $assign_fn for i24 { $($impl)* }); 694 | impl_bin_op!(impl $op = $assign $assign_fn for &i24 { $($impl)* }); 695 | )+}; 696 | 697 | (impl $op: ident = $assign: ident $assign_fn: ident for $ty:ty { 698 | fn $meth: ident($self: tt, $other: ident) { 699 | $($impl: tt)* 700 | } 701 | }) => { 702 | impl $op<$ty> for i24 { 703 | type Output = Self; 704 | 705 | #[inline(always)] 706 | fn $meth($self, $other: $ty) -> Self { 707 | $($impl)* 708 | } 709 | } 710 | 711 | impl $op<$ty> for &i24 { 712 | type Output = i24; 713 | 714 | #[inline(always)] 715 | fn $meth(self, other: $ty) -> i24 { 716 | >::$meth(*self, other) 717 | } 718 | } 719 | 720 | impl $assign<$ty> for i24 { 721 | #[inline(always)] 722 | fn $assign_fn(&mut self, rhs: $ty) { 723 | *self = $op::$meth(*self, rhs) 724 | } 725 | } 726 | }; 727 | } 728 | 729 | impl_bin_op! { 730 | impl Add = AddAssign add_assign { 731 | fn add(self, other) { 732 | // we use twos compliment and so signed and unsigned addition are strictly the same 733 | // so no need to cast to an i32 734 | Self::from_bits_truncate(self.to_bits().wrapping_add(other.to_bits())) 735 | } 736 | } 737 | 738 | impl Sub = SubAssign sub_assign { 739 | fn sub(self, other) { 740 | // we use twos compliment and so signed and unsigned subtraction are strictly the same 741 | // so no need to cast to an i32 742 | Self::from_bits_truncate(self.to_bits().wrapping_sub(other.to_bits())) 743 | } 744 | } 745 | 746 | impl Mul = MulAssign mul_assign { 747 | fn mul(self, other) { 748 | // we use twos compliment and so signed and unsigned non-widening multiplication are strictly the same 749 | // so no need to cast to an i32 750 | Self::from_bits_truncate(self.to_bits().wrapping_mul(other.to_bits())) 751 | } 752 | } 753 | 754 | impl Div = DivAssign div_assign { 755 | fn div(self, other) { 756 | let result = self.to_i32().wrapping_div(other.to_i32()); 757 | Self::wrapping_from_i32(result) 758 | } 759 | } 760 | 761 | impl Rem = RemAssign rem_assign { 762 | fn rem(self, other) { 763 | let result = self.to_i32().wrapping_rem(other.to_i32()); 764 | Self::wrapping_from_i32(result) 765 | } 766 | } 767 | 768 | 769 | impl BitAnd = BitAndAssign bitand_assign { 770 | fn bitand(self, rhs) { 771 | let bits = self.to_bits() & rhs.to_bits(); 772 | // Safety: 773 | // since we and 2 values that both have the most significant byte set to zero 774 | // the output will always have the most significant byte set to zero 775 | unsafe { i24::from_bits(bits) } 776 | } 777 | } 778 | 779 | impl BitOr = BitOrAssign bitor_assign { 780 | fn bitor(self, rhs) { 781 | let bits = self.to_bits() | rhs.to_bits(); 782 | // Safety: 783 | // since we and 2 values that both have the most significant byte set to zero 784 | // the output will always have the most significant byte set to zero 785 | unsafe { i24::from_bits(bits) } 786 | } 787 | } 788 | 789 | impl BitXor = BitXorAssign bitxor_assign { 790 | fn bitxor(self, rhs) { 791 | let bits = self.to_bits() ^ rhs.to_bits(); 792 | // Safety: 793 | // since we and 2 values that both have the most significant byte set to zero 794 | // the output will always have the most significant byte set to zero 795 | unsafe { i24::from_bits(bits) } 796 | } 797 | } 798 | } 799 | 800 | impl Neg for i24 { 801 | type Output = Self; 802 | 803 | #[inline(always)] 804 | fn neg(self) -> Self { 805 | // this is how you negate twos compliment numbers 806 | i24::from_bits_truncate((!self.to_bits()) + 1) 807 | } 808 | } 809 | 810 | impl Not for i24 { 811 | type Output = Self; 812 | 813 | #[inline(always)] 814 | fn not(self) -> Self { 815 | i24::from_bits_truncate(!self.to_bits()) 816 | } 817 | } 818 | 819 | impl Shl for i24 { 820 | type Output = Self; 821 | 822 | #[inline(always)] 823 | fn shl(self, rhs: u32) -> Self::Output { 824 | Self::from_bits_truncate(self.to_bits() << rhs) 825 | } 826 | } 827 | 828 | impl Shr for i24 { 829 | type Output = Self; 830 | 831 | #[inline(always)] 832 | fn shr(self, rhs: u32) -> Self::Output { 833 | // Safety: 834 | // we do a logical shift right by 8 at the end 835 | // and so the most significant octet/byte is set to 0 836 | 837 | // logic: 838 | // <8 bits empty> 839 | // we shift everything up by 8 840 | // <8 bits empty> 841 | // then we do an arithmetic shift 842 | // <8 - n bits empty> 843 | // after we shift everything down by 8 844 | // <8 bits empty> 845 | unsafe { Self::from_bits(((self.to_bits() << 8) as i32 >> rhs) as u32 >> 8) } 846 | } 847 | } 848 | 849 | impl ShrAssign for i24 { 850 | #[inline(always)] 851 | fn shr_assign(&mut self, rhs: u32) { 852 | *self = Shr::shr(*self, rhs) 853 | } 854 | } 855 | 856 | impl ShlAssign for i24 { 857 | #[inline(always)] 858 | fn shl_assign(&mut self, rhs: u32) { 859 | *self = Shl::shl(*self, rhs) 860 | } 861 | } 862 | 863 | macro_rules! impl_fmt { 864 | ($(impl $name: path)+) => {$( 865 | impl $name for i24 { 866 | #[inline] 867 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 868 | ::fmt(&self.to_i32(), f) 869 | } 870 | } 871 | )*}; 872 | } 873 | 874 | macro_rules! impl_bits_fmt { 875 | ($(impl $name: path)+) => {$( 876 | impl $name for i24 { 877 | #[inline(always)] 878 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 879 | ::fmt(self.as_bits(), f) 880 | } 881 | } 882 | )*}; 883 | } 884 | 885 | impl_fmt! { 886 | impl Display 887 | impl Debug 888 | } 889 | 890 | impl_bits_fmt! { 891 | impl UpperHex 892 | impl LowerHex 893 | 894 | impl Octal 895 | impl fmt::Binary 896 | } 897 | 898 | #[cfg(feature = "serde")] 899 | mod serde { 900 | impl serde::Serialize for crate::i24 { 901 | fn serialize(&self, serializer: S) -> Result 902 | where 903 | S: serde::Serializer, 904 | { 905 | serializer.serialize_i32(self.to_i32()) 906 | } 907 | } 908 | 909 | impl<'de> serde::Deserialize<'de> for crate::i24 { 910 | fn deserialize(deserializer: D) -> Result 911 | where 912 | D: serde::Deserializer<'de>, 913 | { 914 | deserializer.deserialize_any(I24Visitor) 915 | } 916 | } 917 | 918 | struct I24Visitor; 919 | 920 | macro_rules! impl_deserialize_infallible { 921 | ($([$ty: path, $visit: ident, $from: ident])+) => {$( 922 | fn $visit(self, v: $ty) -> Result { 923 | Ok(crate::i24::$from(v)) 924 | } 925 | )*}; 926 | } 927 | 928 | macro_rules! impl_deserialize_fallible { 929 | ($([$ty: path, $visit: ident, $try_from: ident])+) => {$( 930 | fn $visit(self, v: $ty) -> Result where E: serde::de::Error { 931 | crate::i24::$try_from(v).ok_or_else(|| E::custom("i24 out of range!")) 932 | } 933 | )*}; 934 | } 935 | 936 | impl serde::de::Visitor<'_> for I24Visitor { 937 | type Value = crate::i24; 938 | 939 | fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { 940 | formatter.write_str("an integer between -2^23 and 2^23") 941 | } 942 | 943 | impl_deserialize_infallible! { 944 | [u8, visit_u8, from_u8] 945 | [i8, visit_i8, from_i8] 946 | [u16, visit_u16, from_u16] 947 | [i16, visit_i16, from_i16] 948 | } 949 | 950 | impl_deserialize_fallible! { 951 | [u32, visit_u32, try_from_u32] 952 | [i32, visit_i32, try_from_i32] 953 | [u64, visit_u64, try_from_u64] 954 | [i64, visit_i64, try_from_i64] 955 | [u128, visit_u128, try_from_u128] 956 | [i128, visit_i128, try_from_i128] 957 | } 958 | } 959 | } 960 | 961 | impl Hash for i24 { 962 | fn hash(&self, state: &mut H) { 963 | I24Repr::hash(&self.0, state) 964 | } 965 | 966 | fn hash_slice(data: &[Self], state: &mut H) 967 | where 968 | Self: Sized, 969 | { 970 | // i24 is repr(transparent) 971 | I24Repr::hash_slice( 972 | unsafe { core::mem::transmute::<&[Self], &[I24Repr]>(data) }, 973 | state, 974 | ) 975 | } 976 | } 977 | 978 | impl ToBytes for i24 { 979 | type Bytes = [u8; 3]; 980 | 981 | fn to_be_bytes(&self) -> Self::Bytes { 982 | self.0.to_be_bytes() 983 | } 984 | 985 | fn to_le_bytes(&self) -> Self::Bytes { 986 | self.0.to_le_bytes() 987 | } 988 | } 989 | 990 | #[cfg(feature = "pyo3")] 991 | #[pyclass(name = "I24")] 992 | pub struct PyI24 { 993 | pub value: i24, 994 | } 995 | 996 | #[cfg(feature = "pyo3")] 997 | #[pymodule(name = "i24")] 998 | fn pyi24(m: &Bound<'_, PyModule>) -> PyResult<()> { 999 | m.add_class::()?; 1000 | Ok(()) 1001 | } 1002 | 1003 | #[cfg(feature = "pyo3")] 1004 | unsafe impl numpy::Element for i24 { 1005 | const IS_COPY: bool = true; 1006 | 1007 | fn get_dtype_bound(py: Python<'_>) -> Bound<'_, numpy::PyArrayDescr> { 1008 | numpy::dtype::(py) 1009 | } 1010 | 1011 | fn clone_ref(&self, _py: Python<'_>) -> Self { 1012 | self.clone() 1013 | } 1014 | 1015 | fn get_dtype(py: Python<'_>) -> Bound<'_, PyArrayDescr> { 1016 | numpy::dtype::(py) 1017 | } 1018 | } 1019 | 1020 | #[cfg(test)] 1021 | mod i24_tests { 1022 | extern crate std; 1023 | 1024 | use super::*; 1025 | use std::format; 1026 | use std::num::IntErrorKind; 1027 | 1028 | #[test] 1029 | fn test_arithmetic_operations() { 1030 | let a = i24!(100); 1031 | let b = i24!(50); 1032 | 1033 | assert_eq!(a + b, i24!(150)); 1034 | assert_eq!(a - b, i24!(50)); 1035 | assert_eq!(a * b, i24!(5000)); 1036 | assert_eq!(a / b, i24!(2)); 1037 | assert_eq!(a % b, i24!(0)); 1038 | } 1039 | 1040 | #[test] 1041 | fn test_negative_operations() { 1042 | let a = i24!(100); 1043 | let b = i24!(-50); 1044 | 1045 | assert_eq!(a + b, i24!(50)); 1046 | assert_eq!(a - b, i24!(150)); 1047 | assert_eq!(a * b, i24!(-5000)); 1048 | assert_eq!(a / b, i24!(-2)); 1049 | } 1050 | 1051 | #[test] 1052 | fn test_bitwise_operations() { 1053 | let a = i24!(0b101010); 1054 | let b = i24!(0b110011); 1055 | 1056 | assert_eq!(a & b, i24!(0b100010)); 1057 | assert_eq!(a | b, i24!(0b111011)); 1058 | assert_eq!(a ^ b, i24!(0b011001)); 1059 | assert_eq!(a << 2, i24!(0b10101000)); 1060 | assert_eq!(a >> 2, i24!(0b1010)); 1061 | } 1062 | 1063 | #[test] 1064 | fn test_checked_addition() { 1065 | assert_eq!(i24!(10).checked_add(i24!(20)), Some(i24!(30))); 1066 | assert_eq!(i24!(10).checked_add(i24!(-20)), Some(i24!(-10))); 1067 | // Overflow cases 1068 | assert_eq!(i24::MAX.checked_add(i24::one()), None); 1069 | assert_eq!( 1070 | (i24::MAX - i24::one()).checked_add(i24::one() * i24!(2)), 1071 | None 1072 | ); 1073 | } 1074 | 1075 | #[test] 1076 | fn test_checked_subtraction() { 1077 | assert_eq!(i24!(10).checked_sub(i24!(20)), Some(i24!(-10))); 1078 | assert_eq!(i24!(10).checked_sub(i24!(-20)), Some(i24!(30))); 1079 | 1080 | // Overflow cases 1081 | assert_eq!(i24::MIN.checked_sub(i24::one()), None); 1082 | assert_eq!( 1083 | (i24::MIN + i24::one()).checked_sub(i24::one() * i24!(2)), 1084 | None 1085 | ); 1086 | } 1087 | 1088 | #[test] 1089 | fn test_checked_division() { 1090 | assert_eq!(i24!(20).checked_div(i24!(5)), Some(i24!(4))); 1091 | assert_eq!(i24!(20).checked_div(i24!(0)), None); 1092 | } 1093 | 1094 | #[test] 1095 | fn test_checked_multiplication() { 1096 | assert_eq!(i24!(5).checked_mul(i24!(6)), Some(i24!(30))); 1097 | assert_eq!(i24::MAX.checked_mul(i24!(2)), None); 1098 | } 1099 | 1100 | #[test] 1101 | fn test_checked_remainder() { 1102 | assert_eq!(i24!(20).checked_rem(i24!(5)), Some(i24!(0))); 1103 | assert_eq!(i24!(20).checked_rem(i24!(0)), None); 1104 | } 1105 | 1106 | #[test] 1107 | fn test_unary_operations() { 1108 | let a = i24!(100); 1109 | 1110 | assert_eq!(-a, i24!(-100)); 1111 | assert_eq!(!a, i24!(-101)); 1112 | } 1113 | 1114 | #[test] 1115 | fn test_from_bytes() { 1116 | let le = i24!(0x030201); 1117 | let be = i24!(0x010203); 1118 | assert_eq!( 1119 | i24::from_ne_bytes([0x01, 0x02, 0x03]), 1120 | if cfg!(target_endian = "big") { be } else { le } 1121 | ); 1122 | assert_eq!(i24::from_le_bytes([0x01, 0x02, 0x03]), le); 1123 | assert_eq!(i24::from_be_bytes([0x01, 0x02, 0x03]), be); 1124 | } 1125 | 1126 | #[test] 1127 | fn test_zero_and_one() { 1128 | assert_eq!(i24::zero(), i24::try_from_i32(0).unwrap()); 1129 | 1130 | assert_eq!(i24::zero(), i24!(0)); 1131 | assert_eq!(i24::one(), i24!(1)); 1132 | } 1133 | 1134 | #[test] 1135 | fn test_from_str() { 1136 | assert_eq!(i24::from_str("100").unwrap(), i24!(100)); 1137 | assert_eq!(i24::from_str("-100").unwrap(), i24!(-100)); 1138 | assert_eq!(i24::from_str(&format!("{}", i24::MAX)).unwrap(), i24::MAX); 1139 | assert_eq!(i24::from_str(&format!("{}", i24::MIN)).unwrap(), i24::MIN); 1140 | assert_eq!( 1141 | *i24::from_str("8388608").unwrap_err().kind(), 1142 | IntErrorKind::PosOverflow 1143 | ); 1144 | assert_eq!( 1145 | *i24::from_str("-8388609").unwrap_err().kind(), 1146 | IntErrorKind::NegOverflow 1147 | ); 1148 | } 1149 | 1150 | #[test] 1151 | fn test_display() { 1152 | assert_eq!(format!("{}", i24!(100)), "100"); 1153 | assert_eq!(format!("{}", i24!(-100)), "-100"); 1154 | } 1155 | 1156 | #[test] 1157 | fn test_wrapping_behavior() { 1158 | assert_eq!(i24::MAX + i24::one(), i24::MIN); 1159 | assert_eq!(i24::MAX + i24::one() + i24::one(), i24::MIN + i24::one()); 1160 | 1161 | assert_eq!(i24::MIN - i24::one(), i24::MAX); 1162 | assert_eq!(i24::MIN - (i24::one() + i24::one()), i24::MAX - i24::one()); 1163 | 1164 | assert_eq!(-i24::MIN, i24::MIN) 1165 | } 1166 | 1167 | #[test] 1168 | fn discriminant_optimization() { 1169 | // this isn't guaranteed by rustc, but this should still hold true 1170 | // if this fails because rustc stops doing it, just remove this test 1171 | // otherwise investigate why this isn't working 1172 | assert_eq!(size_of::(), size_of::>()); 1173 | assert_eq!(size_of::(), size_of::>>()); 1174 | assert_eq!(size_of::(), size_of::>>>()); 1175 | assert_eq!( 1176 | size_of::(), 1177 | size_of::>>>>() 1178 | ); 1179 | } 1180 | 1181 | #[test] 1182 | fn test_shift_operations() { 1183 | let a = i24!(0b1); 1184 | 1185 | // Left shift 1186 | assert_eq!(a << 23, i24::MIN); // 0x800000, which is the minimum negative value 1187 | assert_eq!(a << 24, i24::zero()); // Shifts out all bits 1188 | 1189 | // Right shift 1190 | let b = i24!(-1); // All bits set 1191 | assert_eq!(b >> 1, i24!(-1)); // Sign extension 1192 | assert_eq!(b >> 23, i24!(-1)); // Still all bits set due to sign extension 1193 | assert_eq!(b >> 24, i24!(-1)); // No change after 23 bits 1194 | 1195 | // Edge case: maximum positive value 1196 | let c = i24!(0x7FFFFF); // 8388607 1197 | assert_eq!(c << 1, i24!(-2)); // 0xFFFFFE in 24-bit, which is -2 when sign-extended 1198 | 1199 | // Edge case: minimum negative value 1200 | let d = i24::MIN; // (-0x800000) 1201 | assert_eq!(d >> 1, i24!(-0x400000)); 1202 | assert_eq!(d >> 2, i24!(-0x200000)); 1203 | assert_eq!(d >> 3, i24!(-0x100000)); 1204 | assert_eq!(d >> 4, i24!(-0x080000)); 1205 | 1206 | // Additional test for left shift wrapping 1207 | assert_eq!(c << 1, i24!(-2)); // 0xFFFFFE 1208 | assert_eq!(c << 2, i24!(-4)); // 0xFFFFFC 1209 | assert_eq!(c << 3, i24!(-8)); // 0xFFFFF8 1210 | } 1211 | 1212 | #[test] 1213 | fn test_to_from_i32() { 1214 | for i in I24Repr::MIN..=I24Repr::MAX { 1215 | assert_eq!(i24::try_from_i32(i).unwrap().to_i32(), i) 1216 | } 1217 | } 1218 | 1219 | #[test] 1220 | fn test_from() { 1221 | macro_rules! impl_t { 1222 | ($($ty: ty),+) => {{$( 1223 | for x in <$ty>::MIN..=<$ty>::MAX { 1224 | assert_eq!(<$ty>::try_from(i24::from(x).to_i32()).unwrap(), x) 1225 | } 1226 | )+}}; 1227 | } 1228 | 1229 | assert_eq!(i24::from(true), i24::one()); 1230 | assert_eq!(i24::from(false), i24::zero()); 1231 | 1232 | impl_t!(i8, i16, u8, u16) 1233 | } 1234 | 1235 | #[test] 1236 | fn test_try_from() { 1237 | macro_rules! impl_t { 1238 | (signed $($ty: ty),+) => {{$( 1239 | for x in I24Repr::MIN..=I24Repr::MAX { 1240 | assert_eq!(i24::try_from(<$ty>::from(x)).unwrap().to_i32(), x) 1241 | } 1242 | )+}}; 1243 | 1244 | (unsigned $($ty: ty),+) => {{$( 1245 | for x in 0..=I24Repr::MAX { 1246 | assert_eq!(i24::try_from(<$ty>::try_from(x).unwrap()).unwrap().to_i32(), x) 1247 | } 1248 | )+}}; 1249 | } 1250 | 1251 | impl_t!(signed i32, i64, i128); 1252 | impl_t!(unsigned u32, u64, u128); 1253 | } 1254 | 1255 | #[test] 1256 | fn test_to_from_bits() { 1257 | for i in 0..(1 << 24) { 1258 | assert_eq!(i24::from_bits_truncate(i).to_bits(), i) 1259 | } 1260 | } 1261 | 1262 | #[test] 1263 | #[cfg(feature = "serde")] 1264 | fn test_deserialize_json() { 1265 | #[derive(Debug, PartialEq, ::serde::Deserialize)] 1266 | struct TestStruct { 1267 | value: i24, 1268 | } 1269 | 1270 | let test: TestStruct = 1271 | serde_json::from_str("{ \"value\": 11 }").expect("Failed to deserialize!"); 1272 | let expected = TestStruct { 1273 | value: i24::from_u8(11), 1274 | }; 1275 | 1276 | assert_eq!(test, expected); 1277 | } 1278 | 1279 | #[test] 1280 | #[cfg(feature = "serde")] 1281 | fn test_serialize_json() { 1282 | #[derive(Debug, PartialEq, ::serde::Serialize)] 1283 | struct TestStruct { 1284 | value: i24, 1285 | } 1286 | 1287 | let test_struct = TestStruct { 1288 | value: i24::from_u8(11), 1289 | }; 1290 | let test = serde_json::to_string(&test_struct).expect("Failed to serialize!"); 1291 | assert_eq!(test, "{\"value\":11}"); 1292 | } 1293 | } 1294 | -------------------------------------------------------------------------------- /src/repr.rs: -------------------------------------------------------------------------------- 1 | use bytemuck::{NoUninit, Zeroable}; 2 | use core::cmp::Ordering; 3 | use core::hash::{Hash, Hasher}; 4 | 5 | #[derive(Debug, Copy, Clone)] 6 | #[repr(u8)] 7 | pub(crate) enum ZeroByte { 8 | Zero = 0, 9 | } 10 | 11 | const _: () = 12 | assert!(align_of::() == align_of::() && size_of::() == size_of::()); 13 | 14 | // Safety: ZeroByte is one single byte that is always initialized to zero 15 | unsafe impl NoUninit for ZeroByte {} 16 | 17 | // Safety: ZeroByte is one single byte that is always initialized to zero 18 | // in fact the only valid bit pattern is zero 19 | unsafe impl Zeroable for ZeroByte {} 20 | 21 | #[cfg(feature = "alloc")] 22 | /// Internal packed 3-byte representation of an [`crate::i24`] value used for zero-copy deserialization. 23 | /// 24 | /// This struct is used internally in conjunction with [`bytemuck::cast_slice`] to reinterpret 25 | /// `[u8]` data as a sequence of 24-bit signed integers. 26 | /// 27 | /// # Safety 28 | /// 29 | /// - It is `#[repr(C, packed)]` and consists of only `[u8; 3]` 30 | /// - Implements `NoUninit` and `AnyBitPattern`, making it safe for binary reinterpretation 31 | /// - Not exposed publicly — intended only for internal buffer conversions 32 | #[repr(C, packed)] 33 | #[derive(Copy, Clone)] 34 | pub(crate) struct DiskI24 { 35 | pub(crate) bytes: [u8; 3], 36 | } 37 | #[cfg(feature = "alloc")] 38 | /// Safety: all zeros is a valid value for DiskI24 39 | unsafe impl bytemuck::Zeroable for DiskI24 {} 40 | // Safety: No padding, all fields are u8s 41 | 42 | #[cfg(feature = "alloc")] 43 | /// Safety: DiskI24 is a packed struct with no padding 44 | /// Any 3 bytes can be interpreted as a valid DiskI24 45 | unsafe impl bytemuck::AnyBitPattern for DiskI24 {} 46 | 47 | #[cfg(feature = "alloc")] 48 | /// Safety: DiskI24 is a packed struct with no padding 49 | /// Any 3 bytes can be interpreted as a valid DiskI24 50 | unsafe impl bytemuck::NoUninit for DiskI24 {} 51 | 52 | #[derive(Debug, Copy, Clone)] 53 | #[repr(C, align(4))] 54 | pub(super) struct BigEndianI24Repr { 55 | // most significant byte at the start 56 | pub(crate) most_significant_byte: ZeroByte, 57 | pub(crate) data: [u8; 3], 58 | } 59 | 60 | #[derive(Debug, Copy, Clone)] 61 | #[repr(C, align(4))] 62 | pub(super) struct LittleEndianI24Repr { 63 | pub(crate) data: [u8; 3], 64 | // most significant byte at the end 65 | pub(crate) most_significant_byte: ZeroByte, 66 | } 67 | 68 | #[cfg(target_endian = "big")] 69 | pub(super) type I24Repr = BigEndianI24Repr; 70 | 71 | #[cfg(target_endian = "little")] 72 | pub(super) type I24Repr = LittleEndianI24Repr; 73 | 74 | const _: () = 75 | assert!(align_of::() == align_of::() && size_of::() == size_of::()); 76 | 77 | // Safety: I24Repr is laid out in memory as a `u32` with the most significant byte set to zero 78 | // Must be NoUninit due to the padding byte. 79 | unsafe impl Zeroable for I24Repr {} 80 | 81 | // Safety: I24 repr is laid out in memory as a `u32` with the most significant byte set to zero. 82 | // Must be NoUninit due to the padding byte. 83 | unsafe impl NoUninit for I24Repr {} 84 | 85 | #[cfg(any( 86 | all(target_endian = "little", target_endian = "big"), 87 | not(any(target_endian = "little", target_endian = "big")) 88 | ))] 89 | compile_error!("unknown endianness"); 90 | 91 | impl I24Repr { 92 | pub(super) const MAX: i32 = (1 << 23) - 1; 93 | pub(super) const MIN: i32 = -(1 << 23); 94 | pub(super) const BITS_MASK: u32 = 0xFFFFFF; 95 | 96 | #[inline] 97 | pub const fn to_i32(self) -> i32 { 98 | ((self.to_bits() as i32) << 8) >> 8 99 | } 100 | 101 | #[inline] 102 | pub const fn wrapping_from_i32(value: i32) -> Self { 103 | let proper_i24 = value as u32 & Self::BITS_MASK; 104 | 105 | // Safety: we only use the first 24 least significant bits (i.e 3 bytes) of the value, 106 | // and the most significant byte is set to zero 107 | // therefore layout guarantees hold true 108 | unsafe { Self::from_bits(proper_i24) } 109 | } 110 | 111 | #[inline] 112 | pub const fn saturating_from_i32(value: i32) -> Self { 113 | // Safety: we only use the first 24 least significant bits (i.e 3 bytes) of the value, 114 | // and the most significant byte is set to zero 115 | // therefore layout guarantees hold true 116 | if value > Self::MAX { 117 | const { Self::wrapping_from_i32(Self::MAX) } 118 | } else if value < Self::MIN { 119 | const { Self::wrapping_from_i32(Self::MIN) } 120 | } else { 121 | unsafe { Self::from_bits(value as u32) } 122 | } 123 | } 124 | 125 | #[inline(always)] 126 | pub const fn from_ne_bytes(bytes: [u8; 3]) -> Self { 127 | Self { 128 | data: bytes, 129 | most_significant_byte: ZeroByte::Zero, 130 | } 131 | } 132 | 133 | #[inline(always)] 134 | pub const fn from_be_bytes(bytes: [u8; 3]) -> Self { 135 | Self::from_ne_bytes(bytes).to_be() 136 | } 137 | 138 | #[inline(always)] 139 | pub const fn from_le_bytes(bytes: [u8; 3]) -> Self { 140 | Self::from_ne_bytes(bytes).to_le() 141 | } 142 | 143 | pub const fn swap_bytes(self) -> Self { 144 | // can't just make a `swap_bytes` without endianness checks 145 | // because it also swaps their `repr`, and so this is easier to do 146 | #[cfg(target_endian = "little")] 147 | { 148 | self.to_be() 149 | } 150 | #[cfg(target_endian = "big")] 151 | { 152 | self.to_le() 153 | } 154 | } 155 | 156 | #[inline(always)] 157 | pub const fn to_be(self) -> Self { 158 | #[cfg(target_endian = "big")] 159 | { 160 | self 161 | } 162 | 163 | #[cfg(target_endian = "little")] 164 | { 165 | Self::from_ne_bytes(self.to_be_repr().data) 166 | } 167 | } 168 | 169 | #[inline(always)] 170 | pub const fn to_le(self) -> Self { 171 | #[cfg(target_endian = "little")] 172 | { 173 | self 174 | } 175 | 176 | #[cfg(target_endian = "big")] 177 | { 178 | Self::from_ne_bytes(self.to_le_repr().data) 179 | } 180 | } 181 | 182 | #[inline(always)] 183 | pub const fn to_ne_bytes(self) -> [u8; 3] { 184 | self.data 185 | } 186 | 187 | #[inline(always)] 188 | pub const fn to_be_bytes(self) -> [u8; 3] { 189 | self.to_be_repr().data 190 | } 191 | 192 | #[inline(always)] 193 | pub const fn to_le_bytes(self) -> [u8; 3] { 194 | self.to_le_repr().data 195 | } 196 | 197 | #[inline] 198 | const fn to_be_repr(self) -> BigEndianI24Repr { 199 | #[cfg(target_endian = "big")] 200 | { 201 | self 202 | } 203 | 204 | #[cfg(target_endian = "little")] 205 | { 206 | let val = self.to_bits().swap_bytes(); 207 | 208 | // Safety: 209 | // since this is little endian, the bytes started off being laid out as 210 | // [data1, data2, data3, zero] 211 | // so after swapping the bytes it turns into 212 | // [zero, data3, data2, data1] 213 | // which is the proper layout for `BigEndianI24Repr` 214 | unsafe { core::mem::transmute::(val) } 215 | } 216 | } 217 | 218 | #[inline] 219 | const fn to_le_repr(self) -> LittleEndianI24Repr { 220 | #[cfg(target_endian = "little")] 221 | { 222 | self 223 | } 224 | 225 | #[cfg(target_endian = "big")] 226 | { 227 | let val = self.to_bits().swap_bytes(); 228 | 229 | // Safety: 230 | // since this is big endian, the bytes started off being laid out as 231 | // [zero, data3, data2, data1] 232 | // so after swapping the bytes it turns into 233 | // [data1, data2, data3, zero] 234 | // which is the proper layout for `LittleEndianI24Repr` 235 | unsafe { std::mem::transmute::(val) }.data 236 | } 237 | } 238 | 239 | #[inline(always)] 240 | pub(super) const fn to_bits(self) -> u32 { 241 | // Safety: I24Repr has the same memory layout as a `u32` 242 | unsafe { core::mem::transmute::(self) } 243 | } 244 | 245 | /// Safety: the most significant byte has to equal 0 246 | #[inline(always)] 247 | pub(super) const unsafe fn from_bits(bits: u32) -> Self { 248 | debug_assert!((bits & Self::BITS_MASK) == bits); 249 | // Safety: upheld by caller 250 | unsafe { core::mem::transmute::(bits) } 251 | } 252 | 253 | #[inline(always)] 254 | pub(super) const fn as_bits(&self) -> &u32 { 255 | // Safety: I24Repr has the same memory layout and alignment as a `u32` 256 | unsafe { core::mem::transmute::<&Self, &u32>(self) } 257 | } 258 | 259 | /// this returns a slice of u32's with the most significant byte set to zero 260 | #[inline(always)] 261 | const fn slice_as_bits(slice: &[Self]) -> &[u32] { 262 | // Safety: I24Repr has the same memory layout and alignment as a `u32` 263 | unsafe { core::mem::transmute::<&[Self], &[u32]>(slice) } 264 | } 265 | 266 | #[inline(always)] 267 | const fn const_eq(&self, other: &Self) -> bool { 268 | (*self.as_bits()) == (*other.as_bits()) 269 | } 270 | } 271 | 272 | macro_rules! impl_infallible_unsigned { 273 | ($($meth: ident: $ty:ty),+) => {$( 274 | impl I24Repr { 275 | #[inline(always)] 276 | pub const fn $meth(x: $ty) -> Self { 277 | const { 278 | assert!(<$ty>::MIN == 0 && <$ty>::BITS < 24); 279 | } 280 | 281 | // checked by the assert above 282 | unsafe { Self::from_bits(x as u32) } 283 | } 284 | } 285 | )+}; 286 | } 287 | 288 | trait BoolLimits { 289 | const MIN: u8 = 0; 290 | const BITS: u32 = 1; 291 | } 292 | 293 | impl BoolLimits for bool {} 294 | 295 | impl_infallible_unsigned! { 296 | from_u8: u8, 297 | from_u16: u16, 298 | from_bool: bool 299 | } 300 | 301 | macro_rules! impl_infallible_signed { 302 | ($($meth: ident: $ty:ty),+) => {$( 303 | impl I24Repr { 304 | #[inline(always)] 305 | pub const fn $meth(x: $ty) -> Self { 306 | const { 307 | assert!(<$ty>::MIN < 0 && <$ty>::BITS < 24); 308 | } 309 | 310 | // at least on x86 (and probably all arches with sign extention) 311 | // this seems like the implementation with the best code gen 312 | // https://rust.godbolt.org/z/eThE5n9s1 -> from_i16_3 313 | 314 | // `x as u32` sign extends in accord to the refrence (https://doc.rust-lang.org/reference/expressions/operator-expr.html#type-cast-expressions) 315 | // if positive this would be just the exact same number 316 | // if negative the sign extention is done for us and all we have to do 317 | // is zero out the high byte 318 | unsafe { Self::from_bits(x as u32 & Self::BITS_MASK) } 319 | } 320 | } 321 | )+}; 322 | } 323 | 324 | impl_infallible_signed! { 325 | from_i8: i8, 326 | from_i16: i16 327 | } 328 | 329 | macro_rules! impl_fallible_unsigned { 330 | ($($meth: ident: $ty:ty),+) => {$( 331 | impl I24Repr { 332 | #[inline(always)] 333 | pub const fn $meth(x: $ty) -> Option { 334 | const { assert!(<$ty>::MIN == 0 && <$ty>::BITS > 24) } 335 | 336 | // the 2 impls have equivlent codegen 337 | // https://rust.godbolt.org/z/nE7nzGKPT 338 | if x > const { Self::MAX as $ty } { 339 | return None 340 | } 341 | 342 | // Safety: x is <= Self::MAX meaning the msb is 0 343 | Some(unsafe { Self::from_bits(x as u32) }) 344 | } 345 | } 346 | )+}; 347 | } 348 | 349 | impl_fallible_unsigned! { 350 | try_from_u32: u32, 351 | try_from_u64: u64, 352 | try_from_u128: u128 353 | } 354 | 355 | macro_rules! impl_fallible_signed { 356 | ($($meth: ident: $ty:ty),+) => {$( 357 | impl I24Repr { 358 | #[inline(always)] 359 | pub const fn $meth(x: $ty) -> Option { 360 | const { assert!(<$ty>::MIN < 0 && <$ty>::BITS > 24) } 361 | 362 | if x < const { Self::MIN as $ty } || x > const { Self::MAX as $ty } { 363 | return None 364 | } 365 | 366 | 367 | // this cast already sign extends as the source is signed 368 | // so we get a valid twos compliment number 369 | 370 | // Safety: we zero off the msb 371 | Some(unsafe { Self::from_bits(x as u32 & Self::BITS_MASK) }) 372 | } 373 | } 374 | )+}; 375 | } 376 | 377 | impl_fallible_signed! { 378 | try_from_i32: i32, 379 | try_from_i64: i64, 380 | try_from_i128: i128 381 | } 382 | 383 | impl PartialOrd for I24Repr { 384 | fn partial_cmp(&self, other: &Self) -> Option { 385 | Some(self.cmp(other)) 386 | } 387 | } 388 | 389 | impl Ord for I24Repr { 390 | fn cmp(&self, other: &Self) -> Ordering { 391 | ::cmp(&self.to_i32(), &other.to_i32()) 392 | } 393 | } 394 | 395 | impl PartialEq for I24Repr { 396 | #[inline(always)] 397 | fn eq(&self, other: &Self) -> bool { 398 | I24Repr::const_eq(self, other) 399 | } 400 | } 401 | 402 | impl Eq for I24Repr {} 403 | 404 | impl Hash for I24Repr { 405 | #[inline(always)] 406 | fn hash(&self, state: &mut H) { 407 | u32::hash(self.as_bits(), state) 408 | } 409 | 410 | #[inline(always)] 411 | fn hash_slice(data: &[Self], state: &mut H) 412 | where 413 | Self: Sized, 414 | { 415 | u32::hash_slice(I24Repr::slice_as_bits(data), state) 416 | } 417 | } 418 | 419 | impl Default for I24Repr { 420 | fn default() -> Self { 421 | I24Repr::zeroed() 422 | } 423 | } 424 | 425 | const _: () = { 426 | macro_rules! unwrap { 427 | ($e: expr) => { 428 | match $e { 429 | Some(x) => x, 430 | None => panic!("`unwrap` failed"), 431 | } 432 | }; 433 | } 434 | 435 | // test arbitrary numbers 436 | assert!(I24Repr::const_eq( 437 | &unwrap!(I24Repr::try_from_i32( 438 | unwrap!(I24Repr::try_from_i32(349)).to_i32() - 1897 439 | )), 440 | &unwrap!(I24Repr::try_from_i32(349 - 1897)) 441 | )); 442 | 443 | // test MIN 444 | assert!(unwrap!(I24Repr::try_from_i32(I24Repr::MIN)).to_i32() == I24Repr::MIN); 445 | 446 | // test MIN 447 | assert!(unwrap!(I24Repr::try_from_i32(I24Repr::MAX)).to_i32() == I24Repr::MAX); 448 | }; 449 | 450 | const _: () = { 451 | // ZeroByte layout checks 452 | assert!(size_of::() == 1, "ZeroByte should be 1 byte"); 453 | assert!( 454 | align_of::() == 1, 455 | "ZeroByte should have alignment 1" 456 | ); 457 | 458 | // BigEndianI24Repr layout checks 459 | assert!( 460 | size_of::() == 4, 461 | "BigEndianI24Repr should be 4 bytes" 462 | ); 463 | assert!( 464 | align_of::() == 4, 465 | "BigEndianI24Repr should be aligned to 4" 466 | ); 467 | 468 | // LittleEndianI24Repr layout checks 469 | assert!( 470 | size_of::() == 4, 471 | "LittleEndianI24Repr should be 4 bytes" 472 | ); 473 | assert!( 474 | align_of::() == 4, 475 | "LittleEndianI24Repr should be aligned to 4" 476 | ); 477 | 478 | // I24Repr layout check (resolved depending on target endianness) 479 | assert!(size_of::() == 4, "I24Repr should be 4 bytes"); 480 | assert!(align_of::() == 4, "I24Repr should be aligned to 4"); 481 | }; 482 | --------------------------------------------------------------------------------