From d5370bd220c50f7409a9ff4714d4a2404e6a0e45 Mon Sep 17 00:00:00 2001 From: Hitalo Souza <63821277+enghitalo@users.noreply.github.com> Date: Wed, 28 Feb 2024 05:55:33 -0400 Subject: [PATCH] time: microoptimise the Time formating methods (use custom number->string conversion, instead of string interpolation) (#20917) --- vlib/time/format.v | 214 +++++++++++++++++++++-- vlib/time/time_test.v | 80 +++++++-- vlib/v/tests/bench/bench_json_vs_json2.v | 5 + 3 files changed, 268 insertions(+), 31 deletions(-) diff --git a/vlib/time/format.v b/vlib/time/format.v index d3e2f9f280d3de..65aedd33c536ea 100644 --- a/vlib/time/format.v +++ b/vlib/time/format.v @@ -5,54 +5,243 @@ module time import strings +// int_to_byte_array_no_pad fulfill buffer by part +// it doesn't pad with leading zeros for performance reasons +@[direct_array_access] +fn int_to_byte_array_no_pad(value int, mut arr []u8, size int) { + mut num := value + if size <= 0 || num < 0 { + return + } + + // Start from the end of the array + mut i := size - 1 + + // Convert each digit to a character and store it in the array + for num > 0 && i >= 0 { + arr[i] = (num % 10) + `0` + num /= 10 + i-- + } +} + // format returns a date string in "YYYY-MM-DD HH:mm" format (24h). +@[manualfree] pub fn (t Time) format() string { - return '${t.year:04d}-${t.month:02d}-${t.day:02d} ${t.hour:02d}:${t.minute:02d}' + mut buf := [u8(`0`), `0`, `0`, `0`, `-`, `0`, `0`, `-`, `0`, `0`, ` `, `0`, `0`, `:`, `0`, + `0`] + + defer { + unsafe { buf.free() } + } + + int_to_byte_array_no_pad(t.year, mut buf, 4) + int_to_byte_array_no_pad(t.month, mut buf, 7) + int_to_byte_array_no_pad(t.day, mut buf, 10) + + int_to_byte_array_no_pad(t.hour, mut buf, 13) + int_to_byte_array_no_pad(t.minute, mut buf, 16) + + return buf.bytestr() } // format_ss returns a date string in "YYYY-MM-DD HH:mm:ss" format (24h). +@[manualfree] pub fn (t Time) format_ss() string { - return '${t.year:04d}-${t.month:02d}-${t.day:02d} ${t.hour:02d}:${t.minute:02d}:${t.second:02d}' + mut buf := [u8(`0`), `0`, `0`, `0`, `-`, `0`, `0`, `-`, `0`, `0`, ` `, `0`, `0`, `:`, `0`, + `0`, `:`, `0`, `0`] + + defer { + unsafe { buf.free() } + } + + int_to_byte_array_no_pad(t.year, mut buf, 4) + int_to_byte_array_no_pad(t.month, mut buf, 7) + int_to_byte_array_no_pad(t.day, mut buf, 10) + + int_to_byte_array_no_pad(t.hour, mut buf, 13) + int_to_byte_array_no_pad(t.minute, mut buf, 16) + int_to_byte_array_no_pad(t.second, mut buf, 19) + + return buf.bytestr() } // format_ss_milli returns a date string in "YYYY-MM-DD HH:mm:ss.123" format (24h). +@[manualfree] pub fn (t Time) format_ss_milli() string { - return '${t.year:04d}-${t.month:02d}-${t.day:02d} ${t.hour:02d}:${t.minute:02d}:${t.second:02d}.${(t.nanosecond / 1_000_000):03d}' + mut buf := [u8(`0`), `0`, `0`, `0`, `-`, `0`, `0`, `-`, `0`, `0`, ` `, `0`, `0`, `:`, `0`, + `0`, `:`, `0`, `0`, `.`, `0`, `0`, `0`] + + defer { + unsafe { buf.free() } + } + + int_to_byte_array_no_pad(t.year, mut buf, 4) + int_to_byte_array_no_pad(t.month, mut buf, 7) + int_to_byte_array_no_pad(t.day, mut buf, 10) + + int_to_byte_array_no_pad(t.hour, mut buf, 13) + int_to_byte_array_no_pad(t.minute, mut buf, 16) + int_to_byte_array_no_pad(t.second, mut buf, 19) + + // Extract and format milliseconds + millis := t.nanosecond / 1_000_000 + int_to_byte_array_no_pad(millis, mut buf, 23) + + return buf.bytestr() } // format_ss_micro returns a date string in "YYYY-MM-DD HH:mm:ss.123456" format (24h). +@[manualfree] pub fn (t Time) format_ss_micro() string { - return '${t.year:04d}-${t.month:02d}-${t.day:02d} ${t.hour:02d}:${t.minute:02d}:${t.second:02d}.${(t.nanosecond / 1_000):06d}' + mut buf := [u8(`0`), `0`, `0`, `0`, `-`, `0`, `0`, `-`, `0`, `0`, ` `, `0`, `0`, `:`, `0`, + `0`, `:`, `0`, `0`, `.`, `0`, `0`, `0`, `0`, `0`, `0`] + + defer { + unsafe { buf.free() } + } + + int_to_byte_array_no_pad(t.year, mut buf, 4) + int_to_byte_array_no_pad(t.month, mut buf, 7) + int_to_byte_array_no_pad(t.day, mut buf, 10) + + int_to_byte_array_no_pad(t.hour, mut buf, 13) + int_to_byte_array_no_pad(t.minute, mut buf, 16) + int_to_byte_array_no_pad(t.second, mut buf, 19) + + // Extract and format microseconds + micros := t.nanosecond / 1_000 + int_to_byte_array_no_pad(micros, mut buf, 26) + + return buf.bytestr() } // format_ss_nano returns a date string in "YYYY-MM-DD HH:mm:ss.123456789" format (24h). +@[manualfree] pub fn (t Time) format_ss_nano() string { - return '${t.year:04d}-${t.month:02d}-${t.day:02d} ${t.hour:02d}:${t.minute:02d}:${t.second:02d}.${t.nanosecond:09d}' + mut buf := [u8(`0`), `0`, `0`, `0`, `-`, `0`, `0`, `-`, `0`, `0`, ` `, `0`, `0`, `:`, `0`, + `0`, `:`, `0`, `0`, `.`, `0`, `0`, `0`, `0`, `0`, `0`, `0`, `0`, `0`] + + defer { + unsafe { buf.free() } + } + + int_to_byte_array_no_pad(t.year, mut buf, 4) + int_to_byte_array_no_pad(t.month, mut buf, 7) + int_to_byte_array_no_pad(t.day, mut buf, 10) + + int_to_byte_array_no_pad(t.hour, mut buf, 13) + int_to_byte_array_no_pad(t.minute, mut buf, 16) + int_to_byte_array_no_pad(t.second, mut buf, 19) + + int_to_byte_array_no_pad(t.nanosecond, mut buf, 29) // Adjusted index for 9 digits + + return buf.bytestr() } // format_rfc3339 returns a date string in "YYYY-MM-DDTHH:mm:ss.123Z" format (24 hours, see https://www.rfc-editor.org/rfc/rfc3339.html) // RFC3339 is an Internet profile, based on the ISO 8601 standard for for representation of dates and times using the Gregorian calendar. // It is intended to improve consistency and interoperability, when representing and using date and time in Internet protocols. -@[markused] +@[manualfree; markused] pub fn (t Time) format_rfc3339() string { - u := t.local_to_utc() - return '${u.year:04d}-${u.month:02d}-${u.day:02d}T${u.hour:02d}:${u.minute:02d}:${u.second:02d}.${(u.nanosecond / 1_000_000):03d}Z' + mut buf := [u8(`0`), `0`, `0`, `0`, `-`, `0`, `0`, `-`, `0`, `0`, `T`, `0`, `0`, `:`, `0`, + `0`, `:`, `0`, `0`, `.`, `0`, `0`, `0`, `Z`] + + defer { + unsafe { buf.free() } + } + + if t.unix == 0 && t.nanosecond == 0 { + return buf.bytestr() + } + + if t.is_local { + utc_time := t.local_to_utc() + int_to_byte_array_no_pad(utc_time.year, mut buf, 4) + int_to_byte_array_no_pad(utc_time.month, mut buf, 7) + int_to_byte_array_no_pad(utc_time.day, mut buf, 10) + int_to_byte_array_no_pad(utc_time.hour, mut buf, 13) + int_to_byte_array_no_pad(utc_time.minute, mut buf, 16) + int_to_byte_array_no_pad(utc_time.second, mut buf, 19) + int_to_byte_array_no_pad(utc_time.nanosecond / 1_000_000, mut buf, 23) + } else { + int_to_byte_array_no_pad(t.year, mut buf, 4) + int_to_byte_array_no_pad(t.month, mut buf, 7) + int_to_byte_array_no_pad(t.day, mut buf, 10) + int_to_byte_array_no_pad(t.hour, mut buf, 13) + int_to_byte_array_no_pad(t.minute, mut buf, 16) + int_to_byte_array_no_pad(t.second, mut buf, 19) + int_to_byte_array_no_pad(t.nanosecond / 1_000_000, mut buf, 23) + } + + return buf.bytestr() } // format_rfc3339_nano returns a date string in "YYYY-MM-DDTHH:mm:ss.123456789Z" format (24 hours, see https://www.rfc-editor.org/rfc/rfc3339.html) +@[manualfree] pub fn (t Time) format_rfc3339_nano() string { - u := t.local_to_utc() - return '${u.year:04d}-${u.month:02d}-${u.day:02d}T${u.hour:02d}:${u.minute:02d}:${u.second:02d}.${(u.nanosecond):09d}Z' + mut buf := [u8(`0`), `0`, `0`, `0`, `-`, `0`, `0`, `-`, `0`, `0`, `T`, `0`, `0`, `:`, `0`, + `0`, `:`, `0`, `0`, `.`, `0`, `0`, `0`, `0`, `0`, `0`, `0`, `0`, `0`, `Z`] + + defer { + unsafe { buf.free() } + } + + if t.unix == 0 && t.nanosecond == 0 { + return buf.bytestr() + } + + if t.is_local { + utc_time := t.local_to_utc() + int_to_byte_array_no_pad(utc_time.year, mut buf, 4) + int_to_byte_array_no_pad(utc_time.month, mut buf, 7) + int_to_byte_array_no_pad(utc_time.day, mut buf, 10) + int_to_byte_array_no_pad(utc_time.hour, mut buf, 13) + int_to_byte_array_no_pad(utc_time.minute, mut buf, 16) + int_to_byte_array_no_pad(utc_time.second, mut buf, 19) + int_to_byte_array_no_pad(utc_time.nanosecond, mut buf, 29) + } else { + int_to_byte_array_no_pad(t.year, mut buf, 4) + int_to_byte_array_no_pad(t.month, mut buf, 7) + int_to_byte_array_no_pad(t.day, mut buf, 10) + int_to_byte_array_no_pad(t.hour, mut buf, 13) + int_to_byte_array_no_pad(t.minute, mut buf, 16) + int_to_byte_array_no_pad(t.second, mut buf, 19) + int_to_byte_array_no_pad(t.nanosecond, mut buf, 29) + } + + return buf.bytestr() } // hhmm returns a date string in "HH:mm" format (24h). +@[manualfree] pub fn (t Time) hhmm() string { - return '${t.hour:02d}:${t.minute:02d}' + mut buf := [u8(`0`), `0`, `:`, `0`, `0`] + + defer { + unsafe { buf.free() } + } + + int_to_byte_array_no_pad(t.hour, mut buf, 2) + int_to_byte_array_no_pad(t.minute, mut buf, 5) + + return buf.bytestr() } // hhmmss returns a date string in "HH:mm:ss" format (24h). +@[manualfree] pub fn (t Time) hhmmss() string { - return '${t.hour:02d}:${t.minute:02d}:${t.second:02d}' + mut buf := [u8(`0`), `0`, `:`, `0`, `0`, `:`, `0`, `0`] + + defer { + unsafe { buf.free() } + } + + int_to_byte_array_no_pad(t.hour, mut buf, 2) + int_to_byte_array_no_pad(t.minute, mut buf, 5) + int_to_byte_array_no_pad(t.second, mut buf, 8) + + return buf.bytestr() } // hhmm12 returns a date string in "hh:mm" format (12h). @@ -75,6 +264,7 @@ pub fn (t Time) md() string { return t.get_fmt_date_str(.space, .mmmd) } +// TODO test, improve performance // appends ordinal suffix to a number fn ordinal_suffix(n int) string { if n > 3 && n < 21 { diff --git a/vlib/time/time_test.v b/vlib/time/time_test.v index ebf11d4b983ddc..9e356614075609 100644 --- a/vlib/time/time_test.v +++ b/vlib/time/time_test.v @@ -1,7 +1,7 @@ import time import math -const time_to_test = time.Time{ +const local_time_to_test = time.Time{ year: 1980 month: 7 day: 11 @@ -10,6 +10,19 @@ const time_to_test = time.Time{ second: 42 nanosecond: 123456789 unix: 332198622 + is_local: true +} + +const utc_time_to_test = time.Time{ + year: 1980 + month: 7 + day: 11 + hour: 21 + minute: 23 + second: 42 + nanosecond: 123456789 + unix: 332198622 + is_local: false } fn test_is_leap_year() { @@ -83,39 +96,64 @@ fn test_unix() { fn test_format_rfc3339() { // assert '1980-07-11T19:23:42.123Z' - res := time_to_test.format_rfc3339() + res := local_time_to_test.format_rfc3339() assert res.ends_with('23:42.123Z') assert res.starts_with('1980-07-1') assert res.contains('T') + + // assert '1980-07-11T19:23:42.123Z' + utc_res := utc_time_to_test.format_rfc3339() + assert utc_res.ends_with('23:42.123Z') + assert utc_res.starts_with('1980-07-1') + assert utc_res.contains('T') } fn test_format_rfc3339_nano() { - res := time_to_test.format_rfc3339_nano() + res := local_time_to_test.format_rfc3339_nano() assert res.ends_with('23:42.123456789Z') assert res.starts_with('1980-07-1') assert res.contains('T') + + utc_res := utc_time_to_test.format_rfc3339_nano() + assert utc_res.ends_with('23:42.123456789Z') + assert utc_res.starts_with('1980-07-1') + assert utc_res.contains('T') } fn test_format_ss() { - assert '11.07.1980 21:23:42' == time_to_test.get_fmt_str(.dot, .hhmmss24, .ddmmyyyy) + assert '11.07.1980 21:23:42' == local_time_to_test.get_fmt_str(.dot, .hhmmss24, .ddmmyyyy) + + assert '11.07.1980 21:23:42' == utc_time_to_test.get_fmt_str(.dot, .hhmmss24, .ddmmyyyy) } fn test_format_ss_milli() { - assert '11.07.1980 21:23:42.123' == time_to_test.get_fmt_str(.dot, .hhmmss24_milli, + assert '11.07.1980 21:23:42.123' == local_time_to_test.get_fmt_str(.dot, .hhmmss24_milli, .ddmmyyyy) - assert '1980-07-11 21:23:42.123' == time_to_test.format_ss_milli() + assert '1980-07-11 21:23:42.123' == local_time_to_test.format_ss_milli() + + assert '11.07.1980 21:23:42.123' == utc_time_to_test.get_fmt_str(.dot, .hhmmss24_milli, + .ddmmyyyy) + assert '1980-07-11 21:23:42.123' == utc_time_to_test.format_ss_milli() } fn test_format_ss_micro() { - assert '11.07.1980 21:23:42.123456' == time_to_test.get_fmt_str(.dot, .hhmmss24_micro, + assert '11.07.1980 21:23:42.123456' == local_time_to_test.get_fmt_str(.dot, .hhmmss24_micro, .ddmmyyyy) - assert '1980-07-11 21:23:42.123456' == time_to_test.format_ss_micro() + assert '1980-07-11 21:23:42.123456' == local_time_to_test.format_ss_micro() + + assert '11.07.1980 21:23:42.123456' == utc_time_to_test.get_fmt_str(.dot, .hhmmss24_micro, + .ddmmyyyy) + assert '1980-07-11 21:23:42.123456' == utc_time_to_test.format_ss_micro() } fn test_format_ss_nano() { - assert '11.07.1980 21:23:42.123456789' == time_to_test.get_fmt_str(.dot, .hhmmss24_nano, + assert '11.07.1980 21:23:42.123456789' == local_time_to_test.get_fmt_str(.dot, .hhmmss24_nano, + .ddmmyyyy) + assert '1980-07-11 21:23:42.123456789' == local_time_to_test.format_ss_nano() + + assert '11.07.1980 21:23:42.123456789' == utc_time_to_test.get_fmt_str(.dot, .hhmmss24_nano, .ddmmyyyy) - assert '1980-07-11 21:23:42.123456789' == time_to_test.format_ss_nano() + assert '1980-07-11 21:23:42.123456789' == utc_time_to_test.format_ss_nano() } fn test_smonth() { @@ -194,23 +232,23 @@ fn test_add() { d_nanoseconds := 13 duration := time.Duration(d_seconds * time.second + d_nanoseconds * time.nanosecond) // dump(duration.debug()) - t1 := time_to_test + t1 := local_time_to_test // dump(t1.debug()) - t2 := time_to_test.add(duration) + t2 := local_time_to_test.add(duration) // dump(t2.debug()) assert t2.second == t1.second + d_seconds assert t2.nanosecond == t1.nanosecond + d_nanoseconds assert t2.unix == t1.unix + d_seconds assert t2.is_local == t1.is_local // - t3 := time_to_test.add(-duration) + t3 := local_time_to_test.add(-duration) // dump(t3.debug()) assert t3.second == t1.second - d_seconds assert t3.nanosecond == t1.nanosecond - d_nanoseconds assert t3.unix == t1.unix - d_seconds assert t3.is_local == t1.is_local // - t4 := time_to_test.as_local() + t4 := local_time_to_test.as_local() // dump(t4.debug()) t5 := t4.add(duration) // dump(t5.debug()) @@ -219,13 +257,15 @@ fn test_add() { fn test_add_days() { num_of_days := 3 - t := time_to_test.add_days(num_of_days) - assert t.day == time_to_test.day + num_of_days - assert t.unix == time_to_test.unix + 86400 * num_of_days + t := local_time_to_test.add_days(num_of_days) + assert t.day == local_time_to_test.day + num_of_days + assert t.unix == local_time_to_test.unix + 86400 * num_of_days } fn test_str() { - assert '1980-07-11 21:23:42' == time_to_test.str() + assert '1980-07-11 21:23:42' == local_time_to_test.str() + + assert '1980-07-11 21:23:42' == utc_time_to_test.str() } // not optimal test but will find obvious bugs @@ -322,7 +362,9 @@ fn test_recursive_local_call() { } fn test_strftime() { - assert '1980 July 11' == time_to_test.strftime('%Y %B %d') + assert '1980 July 11' == local_time_to_test.strftime('%Y %B %d') + + assert '1980 July 11' == utc_time_to_test.strftime('%Y %B %d') } fn test_add_seconds_to_time() { diff --git a/vlib/v/tests/bench/bench_json_vs_json2.v b/vlib/v/tests/bench/bench_json_vs_json2.v index e7de33ed0b7290..9a7d2d38417dc0 100644 --- a/vlib/v/tests/bench/bench_json_vs_json2.v +++ b/vlib/v/tests/bench/bench_json_vs_json2.v @@ -86,7 +86,12 @@ fn benchmark_measure_encode_by_type() ! { println(@FN) dump('👈') measure_json_encode_old_vs_new(StructType[string]{})! + println('time.Time]{}') measure_json_encode_old_vs_new(StructType[time.Time]{})! + println('time.utc()') + measure_json_encode_old_vs_new(StructType[time.Time]{time.utc()})! + println('time.now()') + measure_json_encode_old_vs_new(StructType[time.Time]{time.now()})! measure_json_encode_old_vs_new(StructType[int]{})! measure_json_encode_old_vs_new(StructType[f64]{})! measure_json_encode_old_vs_new(StructType[bool]{})!