├── .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 |
6 |
7 | [](https://crates.io/crates/i24)[](https://docs.rs/i24)
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 | 
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 | 
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::