Skip to content

Commit 4af879a

Browse files
authored
[decimal] Implement ln() function that gets natural logarithm (#36)
This pull request includes several changes related to benchmarks, arithmetic operations, and project configuration. The most important changes are the implementation of `ln()` function that gets natural logarithm, the addition of a new benchmark for the natural logarithm function, improvements to the `Decimal` arithmetic operations, and updates to the project configuration files. ### `ln()` function and method * Implemented `ln()` function that gets natural logarithm. ### Benchmarking Enhancements: * Added a comprehensive benchmark for the `Decimal` natural logarithm function (`ln`) in `benches/bench_ln.mojo` which compares performance against Python's decimal module with 20 diverse test cases. ### Pre-calculated constants: * Added the `constants` module which contains many pre-calculated constants. ### Arithmetic Operation Improvements: * Simplified the `add` and `multiply` functions in `src/decimojo/arithmetics.mojo` by using the `Decimal.from_uint128` method to create `Decimal` objects from `UInt128` values, replacing the previous method of extracting 32-bit components. ### Project Configuration Updates: * Updated `mojoproject.toml` to include new test and benchmark commands for the natural logarithm function. * Corrected the test command for the division operation in `mojoproject.toml`. ### Other Changes: * Added the `ln` function to the `src/decimojo/__init__.mojo` file's import list. * Removed an unused import in `benches/bench_exp.mojo`.
1 parent abb6744 commit 4af879a

File tree

11 files changed

+1597
-232
lines changed

11 files changed

+1597
-232
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ tempCodeRunnerFile.mojo
1616
# log files
1717
*.log
1818
# local files
19-
/test*.mojo
19+
/test*.mojo
20+
local

benches/bench_exp.mojo

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ fn run_benchmark(
7171
var mojo_decimal = Decimal(input_value)
7272
var pydecimal = Python.import_module("decimal")
7373
var py_decimal = pydecimal.Decimal(input_value)
74-
var _py_math = Python.import_module("math")
7574

7675
# Execute the operations once to verify correctness
7776
var mojo_result = dm.exponential.exp(mojo_decimal)

benches/bench_ln.mojo

Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
"""
2+
Comprehensive benchmarks for Decimal natural logarithm function (ln).
3+
Compares performance against Python's decimal module with 20 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_ln_" + 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(
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 ln with Python Decimal ln.
59+
60+
Args:
61+
name: Name of the benchmark case.
62+
input_value: String representation of value for ln(x).
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 = dm.exponential.ln(mojo_decimal)
77+
var py_result = py_decimal.ln()
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+
_ = dm.exponential.ln(mojo_decimal)
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.ln()
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 ln(): " + String(mojo_time) + " ns per iteration",
104+
log_file,
105+
)
106+
log_print(
107+
"Python ln(): " + 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(
123+
"=== DeciMojo Natural Logarithm Function (ln) Benchmark ===", log_file
124+
)
125+
log_print("Time: " + String(datetime.datetime.now().isoformat()), log_file)
126+
127+
# Try to get system info
128+
try:
129+
var platform = Python.import_module("platform")
130+
log_print(
131+
"System: "
132+
+ String(platform.system())
133+
+ " "
134+
+ String(platform.release()),
135+
log_file,
136+
)
137+
log_print("Processor: " + String(platform.processor()), log_file)
138+
log_print(
139+
"Python version: " + String(platform.python_version()), log_file
140+
)
141+
except:
142+
log_print("Could not retrieve system information", log_file)
143+
144+
var iterations = 100
145+
var pydecimal = Python().import_module("decimal")
146+
147+
# Set Python decimal precision to match Mojo's
148+
pydecimal.getcontext().prec = 28
149+
log_print(
150+
"Python decimal precision: " + String(pydecimal.getcontext().prec),
151+
log_file,
152+
)
153+
log_print("Mojo decimal precision: " + String(Decimal.MAX_SCALE), log_file)
154+
155+
# Define benchmark cases
156+
log_print(
157+
"\nRunning natural logarithm function benchmarks with "
158+
+ String(iterations)
159+
+ " iterations each",
160+
log_file,
161+
)
162+
163+
# Case 1: ln(1) = 0
164+
run_benchmark(
165+
"ln(1) = 0",
166+
"1",
167+
iterations,
168+
log_file,
169+
speedup_factors,
170+
)
171+
172+
# Case 2: ln(e) ≈ 1
173+
run_benchmark(
174+
"ln(e) ≈ 1",
175+
"2.718281828459045235360287471",
176+
iterations,
177+
log_file,
178+
speedup_factors,
179+
)
180+
181+
# Case 3: ln(2)
182+
run_benchmark(
183+
"ln(2)",
184+
"2",
185+
iterations,
186+
log_file,
187+
speedup_factors,
188+
)
189+
190+
# Case 4: ln(10)
191+
run_benchmark(
192+
"ln(10)",
193+
"10",
194+
iterations,
195+
log_file,
196+
speedup_factors,
197+
)
198+
199+
# Case 5: ln(0.5)
200+
run_benchmark(
201+
"ln(0.5)",
202+
"0.5",
203+
iterations,
204+
log_file,
205+
speedup_factors,
206+
)
207+
208+
# Case 6: ln(5)
209+
run_benchmark(
210+
"ln(5)",
211+
"5",
212+
iterations,
213+
log_file,
214+
speedup_factors,
215+
)
216+
217+
# Case 7: ln with small positive value
218+
run_benchmark(
219+
"Small positive value",
220+
"1.0001",
221+
iterations,
222+
log_file,
223+
speedup_factors,
224+
)
225+
226+
# Case 8: ln with very small positive value
227+
run_benchmark(
228+
"Very small positive value",
229+
"1.000000001",
230+
iterations,
231+
log_file,
232+
speedup_factors,
233+
)
234+
235+
# Case 9: ln with value slightly less than 1
236+
run_benchmark(
237+
"Value slightly less than 1",
238+
"0.9999",
239+
iterations,
240+
log_file,
241+
speedup_factors,
242+
)
243+
244+
# Case 10: ln with value slightly greater than 1
245+
run_benchmark(
246+
"Value slightly greater than 1",
247+
"1.0001",
248+
iterations,
249+
log_file,
250+
speedup_factors,
251+
)
252+
253+
# Case 11: ln with moderate value
254+
run_benchmark(
255+
"Moderate value",
256+
"7.5",
257+
iterations,
258+
log_file,
259+
speedup_factors,
260+
)
261+
262+
# Case 12: ln with large value
263+
run_benchmark(
264+
"Large value",
265+
"1000",
266+
iterations,
267+
log_file,
268+
speedup_factors,
269+
)
270+
271+
# Case 13: ln with very large value
272+
run_benchmark(
273+
"Very large value",
274+
"1000000000",
275+
iterations,
276+
log_file,
277+
speedup_factors,
278+
)
279+
280+
# Case 14: ln with high precision input
281+
run_benchmark(
282+
"High precision input",
283+
"2.718281828459045235360287471",
284+
iterations,
285+
log_file,
286+
speedup_factors,
287+
)
288+
289+
# Case 15: ln with fractional value
290+
run_benchmark(
291+
"Fractional value",
292+
"0.25",
293+
iterations,
294+
log_file,
295+
speedup_factors,
296+
)
297+
298+
# Case 16: ln with fractional value of many digits
299+
run_benchmark(
300+
"Fractional value with many digits",
301+
"0.12345678901234567890123456789",
302+
iterations,
303+
log_file,
304+
speedup_factors,
305+
)
306+
307+
# Case 17: ln with approximate e value
308+
run_benchmark(
309+
"Approximate e value",
310+
"2.718",
311+
iterations,
312+
log_file,
313+
speedup_factors,
314+
)
315+
316+
# Case 18: ln with larger value
317+
run_benchmark(
318+
"Larger value",
319+
"150",
320+
iterations,
321+
log_file,
322+
speedup_factors,
323+
)
324+
325+
# Case 19: ln with value between 0 and 1
326+
run_benchmark(
327+
"Value between 0 and 1",
328+
"0.75",
329+
iterations,
330+
log_file,
331+
speedup_factors,
332+
)
333+
334+
# Case 20: ln with value close to zero
335+
run_benchmark(
336+
"Value close to zero",
337+
"0.00001",
338+
iterations,
339+
log_file,
340+
speedup_factors,
341+
)
342+
343+
# Calculate average speedup factor
344+
var sum_speedup: Float64 = 0.0
345+
for i in range(len(speedup_factors)):
346+
sum_speedup += speedup_factors[i]
347+
var average_speedup = sum_speedup / Float64(len(speedup_factors))
348+
349+
# Display summary
350+
log_print(
351+
"\n=== Natural Logarithm Function Benchmark Summary ===", log_file
352+
)
353+
log_print("Benchmarked: 20 different ln() cases", log_file)
354+
log_print(
355+
"Each case ran: " + String(iterations) + " iterations", log_file
356+
)
357+
log_print("Average speedup: " + String(average_speedup) + "×", log_file)
358+
359+
# List all speedup factors
360+
log_print("\nIndividual speedup factors:", log_file)
361+
for i in range(len(speedup_factors)):
362+
log_print(
363+
String("Case {}: {}×").format(i + 1, round(speedup_factors[i], 2)),
364+
log_file,
365+
)
366+
367+
# Close the log file
368+
log_file.close()
369+
print("Benchmark completed. Log file closed.")

0 commit comments

Comments
 (0)