Skip to content

Commit 0d6e808

Browse files
authored
[decimal] Implement the log() and the log10() functions (#43)
This pull request includes the `log` and `log10` functions in the `decimojo` library. It also introduces a new benchmark for the `log10` function and includes several updates to the `Decimal` struct in the `decimojo` library. ### Library Updates: * Included `log` and `log10` in the `decimojo` library's `__init__.mojo` file to make them available for import. ### New Benchmark for `log10` Function: * Added a comprehensive benchmark for the `log10` function in `benches/bench_log10.mojo` to compare performance against Python's decimal module with diverse test cases. ### Project Configuration Updates: * Updated `benches/bench.mojo` to include `bench_log10` in the list of benchmarks. * Updated `mojoproject.toml` to include `test_log` and `test_log10` in the test configurations and `bench_log10` in the benchmark configurations. ### `Decimal` Struct Enhancements: * Added `@always_inline` decorator to several static methods and instance methods in `struct Decimal` to improve performance.
1 parent d27ffe7 commit 0d6e808

8 files changed

Lines changed: 1366 additions & 167 deletions

File tree

benches/bench.mojo

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ from bench_round import main as bench_round
1010
from bench_comparison import main as bench_comparison
1111
from bench_exp import main as bench_exp
1212
from bench_ln import main as bench_ln
13+
from bench_log10 import main as bench_log10
1314
from bench_power import main as bench_power
1415
from bench_root import main as bench_root
1516

@@ -27,5 +28,6 @@ fn main() raises:
2728
bench_comparison()
2829
bench_exp()
2930
bench_ln()
31+
bench_log10()
3032
bench_power()
3133
bench_root()

benches/bench_log10.mojo

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
"""
2+
Comprehensive benchmarks for Decimal log10() function.
3+
Compares performance against Python's decimal module with diverse test cases.
4+
"""
5+
6+
from decimojo.prelude import dm, Decimal, RoundingMode
7+
from python import Python, PythonObject
8+
from time import perf_counter_ns
9+
import time
10+
import os
11+
from collections import List
12+
13+
14+
fn open_log_file() raises -> PythonObject:
15+
"""
16+
Creates and opens a log file with a timestamp in the filename.
17+
18+
Returns:
19+
A file object opened for writing.
20+
"""
21+
var python = Python.import_module("builtins")
22+
var datetime = Python.import_module("datetime")
23+
24+
# Create logs directory if it doesn't exist
25+
var log_dir = "./logs"
26+
if not os.path.exists(log_dir):
27+
os.makedirs(log_dir)
28+
29+
# Generate a timestamp for the filename
30+
var timestamp = String(datetime.datetime.now().isoformat())
31+
var log_filename = log_dir + "/benchmark_log10_" + timestamp + ".log"
32+
33+
print("Saving benchmark results to:", log_filename)
34+
return python.open(log_filename, "w")
35+
36+
37+
fn log_print(msg: String, log_file: PythonObject) raises:
38+
"""
39+
Prints a message to both the console and the log file.
40+
41+
Args:
42+
msg: The message to print.
43+
log_file: The file object to write to.
44+
"""
45+
print(msg)
46+
log_file.write(msg + "\n")
47+
log_file.flush() # Ensure the message is written immediately
48+
49+
50+
fn run_benchmark_log10(
51+
name: String,
52+
input_value: String,
53+
iterations: Int,
54+
log_file: PythonObject,
55+
mut speedup_factors: List[Float64],
56+
) raises:
57+
"""
58+
Run a benchmark comparing Mojo Decimal.log10 with Python Decimal.log10.
59+
60+
Args:
61+
name: Name of the benchmark case.
62+
input_value: String representation of the value to compute log10 of.
63+
iterations: Number of iterations to run.
64+
log_file: File object for logging results.
65+
speedup_factors: Mojo List to store speedup factors for averaging.
66+
"""
67+
log_print("\nBenchmark: " + name, log_file)
68+
log_print("Input value: " + input_value, log_file)
69+
70+
# Set up Mojo and Python values
71+
var mojo_decimal = Decimal(input_value)
72+
var pydecimal = Python.import_module("decimal")
73+
var py_decimal = pydecimal.Decimal(input_value)
74+
75+
# Execute the operations once to verify correctness
76+
var mojo_result = mojo_decimal.log10()
77+
var py_result = py_decimal.log10()
78+
79+
# Display results for verification
80+
log_print("Mojo result: " + String(mojo_result), log_file)
81+
log_print("Python result: " + String(py_result), log_file)
82+
83+
# Benchmark Mojo implementation
84+
var t0 = perf_counter_ns()
85+
for _ in range(iterations):
86+
_ = mojo_decimal.log10()
87+
var mojo_time = (perf_counter_ns() - t0) / iterations
88+
if mojo_time == 0:
89+
mojo_time = 1 # Prevent division by zero
90+
91+
# Benchmark Python implementation
92+
t0 = perf_counter_ns()
93+
for _ in range(iterations):
94+
_ = py_decimal.log10()
95+
var python_time = (perf_counter_ns() - t0) / iterations
96+
97+
# Calculate speedup factor
98+
var speedup = python_time / mojo_time
99+
speedup_factors.append(Float64(speedup))
100+
101+
# Print results with speedup comparison
102+
log_print(
103+
"Mojo log10(): " + String(mojo_time) + " ns per iteration",
104+
log_file,
105+
)
106+
log_print(
107+
"Python log10(): " + String(python_time) + " ns per iteration",
108+
log_file,
109+
)
110+
log_print("Speedup factor: " + String(speedup), log_file)
111+
112+
113+
fn main() raises:
114+
# Open log file
115+
var log_file = open_log_file()
116+
var datetime = Python.import_module("datetime")
117+
118+
# Create a Mojo List to store speedup factors for averaging later
119+
var speedup_factors = List[Float64]()
120+
121+
# Display benchmark header with system information
122+
log_print("=== DeciMojo log10() Function Benchmark ===", log_file)
123+
log_print("Time: " + String(datetime.datetime.now().isoformat()), log_file)
124+
125+
# Try to get system info
126+
try:
127+
var platform = Python.import_module("platform")
128+
log_print(
129+
"System: "
130+
+ String(platform.system())
131+
+ " "
132+
+ String(platform.release()),
133+
log_file,
134+
)
135+
log_print("Processor: " + String(platform.processor()), log_file)
136+
log_print(
137+
"Python version: " + String(platform.python_version()), log_file
138+
)
139+
except:
140+
log_print("Could not retrieve system information", log_file)
141+
142+
var iterations = 100
143+
var pydecimal = Python().import_module("decimal")
144+
145+
# Set Python decimal precision to match Mojo's
146+
pydecimal.getcontext().prec = 28
147+
log_print(
148+
"Python decimal precision: " + String(pydecimal.getcontext().prec),
149+
log_file,
150+
)
151+
log_print("Mojo decimal precision: " + String(Decimal.MAX_SCALE), log_file)
152+
153+
# Define benchmark cases
154+
log_print(
155+
"\nRunning log10() function benchmarks with "
156+
+ String(iterations)
157+
+ " iterations each",
158+
log_file,
159+
)
160+
161+
# Case 1: Exact power of 10 (10^0)
162+
run_benchmark_log10(
163+
"Power of 10 (10^0)", "1", iterations, log_file, speedup_factors
164+
)
165+
166+
# Case 2: Exact power of 10 (10^1)
167+
run_benchmark_log10(
168+
"Power of 10 (10^1)", "10", iterations, log_file, speedup_factors
169+
)
170+
171+
# Case 3: Exact power of 10 (10^2)
172+
run_benchmark_log10(
173+
"Power of 10 (10^2)", "100", iterations, log_file, speedup_factors
174+
)
175+
176+
# Case 4: Exact power of 10 (10^-1)
177+
run_benchmark_log10(
178+
"Power of 10 (10^-1)", "0.1", iterations, log_file, speedup_factors
179+
)
180+
181+
# Case 5: Exact power of 10 (10^-2)
182+
run_benchmark_log10(
183+
"Power of 10 (10^-2)", "0.01", iterations, log_file, speedup_factors
184+
)
185+
186+
# Case 6: Number between powers of 10 (middle)
187+
run_benchmark_log10(
188+
"Between powers - middle", "5", iterations, log_file, speedup_factors
189+
)
190+
191+
# Case 7: Number between powers of 10 (closer to lower)
192+
run_benchmark_log10(
193+
"Between powers - closer to lower",
194+
"2",
195+
iterations,
196+
log_file,
197+
speedup_factors,
198+
)
199+
200+
# Case 8: Number between powers of 10 (closer to upper)
201+
run_benchmark_log10(
202+
"Between powers - closer to upper",
203+
"9",
204+
iterations,
205+
log_file,
206+
speedup_factors,
207+
)
208+
209+
# Case 9: Decimal number with many digits
210+
run_benchmark_log10(
211+
"Decimal with many digits",
212+
"3.1415926535897932384626433832795",
213+
iterations,
214+
log_file,
215+
speedup_factors,
216+
)
217+
218+
# Case 10: Small number (close to 0)
219+
run_benchmark_log10(
220+
"Small number close to 0",
221+
"0.0000001",
222+
iterations,
223+
log_file,
224+
speedup_factors,
225+
)
226+
227+
# Case 11: Large number
228+
run_benchmark_log10(
229+
"Large number", "1000000000000", iterations, log_file, speedup_factors
230+
)
231+
232+
# Case 12: Number close to 1 (slightly above)
233+
run_benchmark_log10(
234+
"Close to 1 (above)",
235+
"1.00000001",
236+
iterations,
237+
log_file,
238+
speedup_factors,
239+
)
240+
241+
# Case 13: Number close to 1 (slightly below)
242+
run_benchmark_log10(
243+
"Close to 1 (below)",
244+
"0.99999999",
245+
iterations,
246+
log_file,
247+
speedup_factors,
248+
)
249+
250+
# Case 14: Non-integer power of 10 (10^1.5)
251+
run_benchmark_log10(
252+
"Non-integer power of 10 (10^1.5)",
253+
"31.62277660168",
254+
iterations,
255+
log_file,
256+
speedup_factors,
257+
)
258+
259+
# Case 15: Repeating pattern number
260+
run_benchmark_log10(
261+
"Repeating pattern",
262+
"1.234567890123456789",
263+
iterations,
264+
log_file,
265+
speedup_factors,
266+
)
267+
268+
# Case 16: Very precise input requiring full precision
269+
run_benchmark_log10(
270+
"Very precise input",
271+
"2.718281828459045235360287471352662",
272+
iterations,
273+
log_file,
274+
speedup_factors,
275+
)
276+
277+
# Case 17: Integer with many digits
278+
run_benchmark_log10(
279+
"Integer with many digits",
280+
"123456789012345678901234",
281+
iterations,
282+
log_file,
283+
speedup_factors,
284+
)
285+
286+
# Case 18: The square root of 10
287+
run_benchmark_log10(
288+
"Square root of 10",
289+
"3.16227766017",
290+
iterations,
291+
log_file,
292+
speedup_factors,
293+
)
294+
295+
# Case 19: Scientific calculation value
296+
run_benchmark_log10(
297+
"Scientific calculation value",
298+
"299792458",
299+
iterations,
300+
log_file,
301+
speedup_factors,
302+
)
303+
304+
# Case 20: Random decimal value
305+
run_benchmark_log10(
306+
"Random decimal value",
307+
"4.2857142857",
308+
iterations,
309+
log_file,
310+
speedup_factors,
311+
)
312+
313+
# Calculate average speedup factor
314+
var sum_speedup: Float64 = 0.0
315+
for i in range(len(speedup_factors)):
316+
sum_speedup += speedup_factors[i]
317+
var average_speedup = sum_speedup / Float64(len(speedup_factors))
318+
319+
# Display summary
320+
log_print("\n=== log10() Function Benchmark Summary ===", log_file)
321+
log_print("Benchmarked: 20 different log10() cases", log_file)
322+
log_print(
323+
"Each case ran: " + String(iterations) + " iterations", log_file
324+
)
325+
log_print("Average speedup: " + String(average_speedup) + "×", log_file)
326+
327+
# List all speedup factors
328+
log_print("\nIndividual speedup factors:", log_file)
329+
for i in range(len(speedup_factors)):
330+
log_print(
331+
String("Case {}: {}×").format(i + 1, round(speedup_factors[i], 2)),
332+
log_file,
333+
)
334+
335+
# Close the log file
336+
log_file.close()
337+
print("Benchmark completed. Log file closed.")

mojoproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ test_comparison = "magic run package && magic run mojo test tests/test_compariso
4343
test_factorial = "magic run package && magic run mojo test tests/test_factorial.mojo && magic run delete_package"
4444
test_exp = "magic run package && magic run mojo test tests/test_exp.mojo && magic run delete_package"
4545
test_ln = "magic run package && magic run mojo test tests/test_ln.mojo && magic run delete_package"
46+
test_log = "magic run package && magic run mojo test tests/test_log.mojo && magic run delete_package"
47+
test_log10 = "magic run package && magic run mojo test tests/test_log10.mojo && magic run delete_package"
4648
test_power = "magic run package && magic run mojo test tests/test_power.mojo && magic run delete_package"
4749

4850
# benches
@@ -59,6 +61,7 @@ bench_from_int = "magic run package && cd benches && magic run mojo bench_from_i
5961
bench_comparison = "magic run package && cd benches && magic run mojo bench_comparison.mojo && cd .. && magic run delete_package"
6062
bench_exp = "magic run package && cd benches && magic run mojo bench_exp.mojo && cd .. && magic run delete_package"
6163
bench_ln = "magic run package && cd benches && magic run mojo bench_ln.mojo && cd .. && magic run delete_package"
64+
bench_log10 = "magic run package && cd benches && magic run mojo bench_log10.mojo && cd .. && magic run delete_package"
6265
bench_power = "magic run package && cd benches && magic run mojo bench_power.mojo && cd .. && magic run delete_package"
6366

6467
# before commit

src/decimojo/__init__.mojo

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ from .comparison import (
4949
not_equal,
5050
)
5151

52-
from .exponential import power, root, sqrt, exp, ln
52+
from .exponential import power, root, sqrt, exp, ln, log, log10
5353

5454
from .rounding import round
5555

0 commit comments

Comments
 (0)