Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

rtc: Improve performance of rtc_time64_to_tm(). Add tests.

The current implementation of rtc_time64_to_tm() contains unnecessary
loops, branches and look-up tables. The new one uses an arithmetic-based
algorithm appeared in [1] and is approximately 4.3 times faster (YMMV).

The drawback is that the new code isn't intuitive and contains many 'magic
numbers' (not unusual for this type of algorithm). However, [1] justifies
all those numbers and, given this function's history, the code is unlikely
to need much maintenance, if any at all.

Add a KUnit test case that checks every day in a 160,000 years interval
starting on 1970-01-01 against the expected result. Add a new config
RTC_LIB_KUNIT_TEST symbol to give the option to run this test suite.

[1] Neri, Schneider, "Euclidean Affine Functions and Applications to
Calendar Algorithms". https://arxiv.org/abs/2102.06959

Signed-off-by: Cassio Neri <cassio.neri@gmail.com>
Reported-by: kernel test robot <lkp@intel.com>
Signed-off-by: Alexandre Belloni <alexandre.belloni@bootlin.com>
Link: https://lore.kernel.org/r/20210624201343.85441-1-cassio.neri@gmail.com

authored by

Cassio Neri and committed by
Alexandre Belloni
1d1bb12a fffd603a

+168 -25
+10
drivers/rtc/Kconfig
··· 10 10 bool 11 11 select RTC_LIB 12 12 13 + config RTC_LIB_KUNIT_TEST 14 + tristate "KUnit test for RTC lib functions" if !KUNIT_ALL_TESTS 15 + depends on KUNIT 16 + default KUNIT_ALL_TESTS 17 + select RTC_LIB 18 + help 19 + Enable this option to test RTC library functions. 20 + 21 + If unsure, say N. 22 + 13 23 menuconfig RTC_CLASS 14 24 bool "Real Time Clock" 15 25 default n
+1
drivers/rtc/Makefile
··· 178 178 obj-$(CONFIG_RTC_DRV_X1205) += rtc-x1205.o 179 179 obj-$(CONFIG_RTC_DRV_XGENE) += rtc-xgene.o 180 180 obj-$(CONFIG_RTC_DRV_ZYNQMP) += rtc-zynqmp.o 181 + obj-$(CONFIG_RTC_LIB_KUNIT_TEST) += lib_test.o
+78 -25
drivers/rtc/lib.c
··· 6 6 * Author: Alessandro Zummo <a.zummo@towertech.it> 7 7 * 8 8 * based on arch/arm/common/rtctime.c and other bits 9 + * 10 + * Author: Cassio Neri <cassio.neri@gmail.com> (rtc_time64_to_tm) 9 11 */ 10 12 11 13 #include <linux/export.h> ··· 23 21 /* Leap years */ 24 22 { 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 } 25 23 }; 26 - 27 - #define LEAPS_THRU_END_OF(y) ((y) / 4 - (y) / 100 + (y) / 400) 28 24 29 25 /* 30 26 * The number of days in the month. ··· 42 42 } 43 43 EXPORT_SYMBOL(rtc_year_days); 44 44 45 - /* 46 - * rtc_time64_to_tm - Converts time64_t to rtc_time. 47 - * Convert seconds since 01-01-1970 00:00:00 to Gregorian date. 45 + /** 46 + * rtc_time64_to_tm - converts time64_t to rtc_time. 47 + * 48 + * @time: The number of seconds since 01-01-1970 00:00:00. 49 + * (Must be positive.) 50 + * @tm: Pointer to the struct rtc_time. 48 51 */ 49 52 void rtc_time64_to_tm(time64_t time, struct rtc_time *tm) 50 53 { 51 - unsigned int month, year, secs; 54 + unsigned int secs; 52 55 int days; 56 + 57 + u64 u64tmp; 58 + u32 u32tmp, udays, century, day_of_century, year_of_century, year, 59 + day_of_year, month, day; 60 + bool is_Jan_or_Feb, is_leap_year; 53 61 54 62 /* time must be positive */ 55 63 days = div_s64_rem(time, 86400, &secs); ··· 65 57 /* day of the week, 1970-01-01 was a Thursday */ 66 58 tm->tm_wday = (days + 4) % 7; 67 59 68 - year = 1970 + days / 365; 69 - days -= (year - 1970) * 365 70 - + LEAPS_THRU_END_OF(year - 1) 71 - - LEAPS_THRU_END_OF(1970 - 1); 72 - while (days < 0) { 73 - year -= 1; 74 - days += 365 + is_leap_year(year); 75 - } 76 - tm->tm_year = year - 1900; 77 - tm->tm_yday = days + 1; 60 + /* 61 + * The following algorithm is, basically, Proposition 6.3 of Neri 62 + * and Schneider [1]. In a few words: it works on the computational 63 + * (fictitious) calendar where the year starts in March, month = 2 64 + * (*), and finishes in February, month = 13. This calendar is 65 + * mathematically convenient because the day of the year does not 66 + * depend on whether the year is leap or not. For instance: 67 + * 68 + * March 1st 0-th day of the year; 69 + * ... 70 + * April 1st 31-st day of the year; 71 + * ... 72 + * January 1st 306-th day of the year; (Important!) 73 + * ... 74 + * February 28th 364-th day of the year; 75 + * February 29th 365-th day of the year (if it exists). 76 + * 77 + * After having worked out the date in the computational calendar 78 + * (using just arithmetics) it's easy to convert it to the 79 + * corresponding date in the Gregorian calendar. 80 + * 81 + * [1] "Euclidean Affine Functions and Applications to Calendar 82 + * Algorithms". https://arxiv.org/abs/2102.06959 83 + * 84 + * (*) The numbering of months follows rtc_time more closely and 85 + * thus, is slightly different from [1]. 86 + */ 78 87 79 - for (month = 0; month < 11; month++) { 80 - int newdays; 88 + udays = ((u32) days) + 719468; 81 89 82 - newdays = days - rtc_month_days(month, year); 83 - if (newdays < 0) 84 - break; 85 - days = newdays; 86 - } 87 - tm->tm_mon = month; 88 - tm->tm_mday = days + 1; 90 + u32tmp = 4 * udays + 3; 91 + century = u32tmp / 146097; 92 + day_of_century = u32tmp % 146097 / 4; 93 + 94 + u32tmp = 4 * day_of_century + 3; 95 + u64tmp = 2939745ULL * u32tmp; 96 + year_of_century = upper_32_bits(u64tmp); 97 + day_of_year = lower_32_bits(u64tmp) / 2939745 / 4; 98 + 99 + year = 100 * century + year_of_century; 100 + is_leap_year = year_of_century != 0 ? 101 + year_of_century % 4 == 0 : century % 4 == 0; 102 + 103 + u32tmp = 2141 * day_of_year + 132377; 104 + month = u32tmp >> 16; 105 + day = ((u16) u32tmp) / 2141; 106 + 107 + /* 108 + * Recall that January 01 is the 306-th day of the year in the 109 + * computational (not Gregorian) calendar. 110 + */ 111 + is_Jan_or_Feb = day_of_year >= 306; 112 + 113 + /* Converts to the Gregorian calendar. */ 114 + year = year + is_Jan_or_Feb; 115 + month = is_Jan_or_Feb ? month - 12 : month; 116 + day = day + 1; 117 + 118 + day_of_year = is_Jan_or_Feb ? 119 + day_of_year - 306 : day_of_year + 31 + 28 + is_leap_year; 120 + 121 + /* Converts to rtc_time's format. */ 122 + tm->tm_year = (int) (year - 1900); 123 + tm->tm_mon = (int) month; 124 + tm->tm_mday = (int) day; 125 + tm->tm_yday = (int) day_of_year + 1; 89 126 90 127 tm->tm_hour = secs / 3600; 91 128 secs -= tm->tm_hour * 3600;
+79
drivers/rtc/lib_test.c
··· 1 + // SPDX-License-Identifier: LGPL-2.1+ 2 + 3 + #include <kunit/test.h> 4 + #include <linux/rtc.h> 5 + 6 + /* 7 + * Advance a date by one day. 8 + */ 9 + static void advance_date(int *year, int *month, int *mday, int *yday) 10 + { 11 + if (*mday != rtc_month_days(*month - 1, *year)) { 12 + ++*mday; 13 + ++*yday; 14 + return; 15 + } 16 + 17 + *mday = 1; 18 + if (*month != 12) { 19 + ++*month; 20 + ++*yday; 21 + return; 22 + } 23 + 24 + *month = 1; 25 + *yday = 1; 26 + ++*year; 27 + } 28 + 29 + /* 30 + * Checks every day in a 160000 years interval starting on 1970-01-01 31 + * against the expected result. 32 + */ 33 + static void rtc_time64_to_tm_test_date_range(struct kunit *test) 34 + { 35 + /* 36 + * 160000 years = (160000 / 400) * 400 years 37 + * = (160000 / 400) * 146097 days 38 + * = (160000 / 400) * 146097 * 86400 seconds 39 + */ 40 + time64_t total_secs = ((time64_t) 160000) / 400 * 146097 * 86400; 41 + 42 + int year = 1970; 43 + int month = 1; 44 + int mday = 1; 45 + int yday = 1; 46 + 47 + struct rtc_time result; 48 + time64_t secs; 49 + s64 days; 50 + 51 + for (secs = 0; secs <= total_secs; secs += 86400) { 52 + 53 + rtc_time64_to_tm(secs, &result); 54 + 55 + days = div_s64(secs, 86400); 56 + 57 + #define FAIL_MSG "%d/%02d/%02d (%2d) : %ld", \ 58 + year, month, mday, yday, days 59 + 60 + KUNIT_ASSERT_EQ_MSG(test, year - 1900, result.tm_year, FAIL_MSG); 61 + KUNIT_ASSERT_EQ_MSG(test, month - 1, result.tm_mon, FAIL_MSG); 62 + KUNIT_ASSERT_EQ_MSG(test, mday, result.tm_mday, FAIL_MSG); 63 + KUNIT_ASSERT_EQ_MSG(test, yday, result.tm_yday, FAIL_MSG); 64 + 65 + advance_date(&year, &month, &mday, &yday); 66 + } 67 + } 68 + 69 + static struct kunit_case rtc_lib_test_cases[] = { 70 + KUNIT_CASE(rtc_time64_to_tm_test_date_range), 71 + {} 72 + }; 73 + 74 + static struct kunit_suite rtc_lib_test_suite = { 75 + .name = "rtc_lib_test_cases", 76 + .test_cases = rtc_lib_test_cases, 77 + }; 78 + 79 + kunit_test_suite(rtc_lib_test_suite);