diff mbox series

[v2,1/2] util_macros.h: fix/rework find_closest() macros

Message ID 20241105145406.554365-1-aardelean@baylibre.com (mailing list archive)
State New
Headers show
Series [v2,1/2] util_macros.h: fix/rework find_closest() macros | expand

Commit Message

Alexandru Ardelean Nov. 5, 2024, 2:54 p.m. UTC
A bug was found in the find_closest() (find_closest_descending() is also
affected after some testing), where for certain values with small
progressions, the rounding (done by averaging 2 values) causes an incorrect
index to be returned.
The rounding issues occur for progressions of 1, 2 and 3. It goes away when
the progression/interval between two values is 4 or larger.

It's particularly bad for progressions of 1. For example if there's an
array of 'a = { 1, 2, 3 }', using 'find_closest(2, a ...)' would return 0
(the index of '1'), rather than returning 1 (the index of '2').
This means that for exact values (with a progression of 1), find_closest()
will misbehave and return the index of the value smaller than the one we're
searching for.
For progressions of 2 and 3, the exact values are obtained correctly; but
values aren't approximated correctly (as one would expect). Starting with
progressions of 4, all seems to be good (one gets what one would expect).

While one could argue that 'find_closest()' should not be used for arrays
with progressions of 1 (i.e. '{1, 2, 3, ...}', the macro should still
behave correctly.

The bug was found while testing the 'drivers/iio/adc/ad7606.c',
specifically the oversampling feature.
For reference, the oversampling values are listed as:
   static const unsigned int ad7606_oversampling_avail[7] = {
          1, 2, 4, 8, 16, 32, 64,
   };

When doing:
  1. $ echo 1 > /sys/bus/iio/devices/iio\:device0/oversampling_ratio
     $ cat /sys/bus/iio/devices/iio\:device0/oversampling_ratio
     1  # this is fine
  2. $ echo 2 > /sys/bus/iio/devices/iio\:device0/oversampling_ratio
     $ cat /sys/bus/iio/devices/iio\:device0/oversampling_ratio
     1  # this is wrong; 2 should be returned here
  3. $ echo 3 > /sys/bus/iio/devices/iio\:device0/oversampling_ratio
     $ cat /sys/bus/iio/devices/iio\:device0/oversampling_ratio
     2  # this is fine
  4. $ echo 4 > /sys/bus/iio/devices/iio\:device0/oversampling_ratio
     $ cat /sys/bus/iio/devices/iio\:device0/oversampling_ratio
     4  # this is fine
And from here-on, the values are as correct (one gets what one would
expect.)

While writing a kunit test for this bug, a peculiar issue was found for the
array in the 'drivers/hwmon/ina2xx.c' & 'drivers/iio/adc/ina2xx-adc.c'
drivers. While running the kunit test (for 'ina226_avg_tab' from these
drivers):
  * idx = find_closest([-1 to 2], ina226_avg_tab, ARRAY_SIZE(ina226_avg_tab));
    This returns idx == 0, so value.
  * idx = find_closest(3, ina226_avg_tab, ARRAY_SIZE(ina226_avg_tab));
    This returns idx == 0, value 1; and now one could argue whether 3 is
    closer to 4 or to 1. This quirk only appears for value '3' in this
    array, but it seems to be a another rounding issue.
  * And from 4 onwards the 'find_closest'() works fine (one gets what one
    would expect).

This change reworks the find_closest() macros to also check the difference
between the left and right elements when 'x'. If the distance to the right
is smaller (than the distance to the left), the index is incremented by 1.
This also makes redundant the need for using the DIV_ROUND_CLOSEST() macro.

In order to accommodate for any mix of negative + positive values, the
internal variables '__fc_x', '__fc_mid_x', '__fc_left' & '__fc_right' are
forced to 'long' type. This also addresses any potential bugs/issues with
'x' being of an unsigned type. In those situations any comparison between
signed & unsigned would be promoted to a comparison between 2 unsigned
numbers; this is especially annoying when '__fc_left' & '__fc_right'
underflow.

The find_closest_descending() macro was also reworked and duplicated from
the find_closest(), and it is being iterated in reverse. The main reason
for this is to get the same indices as 'find_closest()' (but in reverse).
The comparison for '__fc_right < __fc_left' favors going the array in
ascending order.
For example for array '{ 1024, 512, 256, 128, 64, 16, 4, 1 }' and x = 3, we
get:
    __fc_mid_x = 2
    __fc_left = -1
    __fc_right = -2
    Then '__fc_right < __fc_left' evaluates to true and '__fc_i++' becomes 7
    which is not quite incorrect, but 3 is closer to 4 than to 1.

This change has been validated with the kunit from the next patch.

Fixes: 95d119528b0b ("util_macros.h: add find_closest() macro")
Signed-off-by: Alexandru Ardelean <aardelean@baylibre.com>
---

Changelog v1 -> v2:
* https://lore.kernel.org/linux-iio/20241031063707.795842-1-aardelean@baylibre.com/
* split the __find_closest() macro into find_closest() & find_closest_descending()
  * find_closest_descending() is iterating in reverse order
  * this favors some corner cases with small values
* forcing types for '__fc_x', '__fc_mid_x', '__fc_left' && '__fc_right' to be long
  * this resolves several potential issues with combining arrays of signed/unsigned
    values with 'x' of type signed/unsigned
* fixed error with previous implementation where __fc_mid_x was used instead of __fc_x
  when calculating '__fc_left' && '__fc_right'
* updated commit description with more information (also found on previous
  thread) + the description for the changed implementation

 include/linux/util_macros.h | 56 ++++++++++++++++++++++++++-----------
 1 file changed, 40 insertions(+), 16 deletions(-)

Comments

Andrew Morton Nov. 5, 2024, 11:08 p.m. UTC | #1
On Tue,  5 Nov 2024 16:54:05 +0200 Alexandru Ardelean <aardelean@baylibre.com> wrote:

> A bug was found in the find_closest() (find_closest_descending() is also
> affected after some testing), where for certain values with small
> progressions, the rounding (done by averaging 2 values) causes an incorrect
> index to be returned.

Convincing changelog, thanks.

> -#define find_closest(x, a, as) __find_closest(x, a, as, <=)
> +#define find_closest(x, a, as)						\
> +({									\
> +	typeof(as) __fc_i, __fc_as = (as) - 1;				\
> +	long __fc_mid_x, __fc_x = (x);					\
> +	long __fc_left, __fc_right;					\
> +	typeof(*a) const *__fc_a = (a);					\
> +	for (__fc_i = 0; __fc_i < __fc_as; __fc_i++) {			\
> +		__fc_mid_x = (__fc_a[__fc_i] + __fc_a[__fc_i + 1]) / 2;	\
> +		if (__fc_x <= __fc_mid_x) {				\
> +			__fc_left = __fc_x - __fc_a[__fc_i];		\
> +			__fc_right = __fc_a[__fc_i + 1] - __fc_x;	\
> +			if (__fc_right < __fc_left)			\
> +				__fc_i++;				\
> +			break;						\
> +		}							\
> +	}								\
> +	(__fc_i);							\
> +})
>
> ...
>
> +#define find_closest_descending(x, a, as)				\

Boy these things are hard to read.  They're also bloaty and I'm
counting 36ish callsites!

Can we fix both issues by just giving up on the macro approach and
reimplement them in out-of-line C code?  All the sites I looked at are
using 32-bit quantities - a mix of signed and unsigned.  

It's separate from this bugfix of course, but would it be feasible for
someone to go switch all callers to use u32's then reimplement these in
lib/find_closest.c?
Alexandru Ardelean Nov. 6, 2024, 2:03 p.m. UTC | #2
On Wed, Nov 6, 2024 at 1:08 AM Andrew Morton <akpm@linux-foundation.org> wrote:
>
> On Tue,  5 Nov 2024 16:54:05 +0200 Alexandru Ardelean <aardelean@baylibre.com> wrote:
>
> > A bug was found in the find_closest() (find_closest_descending() is also
> > affected after some testing), where for certain values with small
> > progressions, the rounding (done by averaging 2 values) causes an incorrect
> > index to be returned.
>
> Convincing changelog, thanks.
>
> > -#define find_closest(x, a, as) __find_closest(x, a, as, <=)
> > +#define find_closest(x, a, as)                                               \
> > +({                                                                   \
> > +     typeof(as) __fc_i, __fc_as = (as) - 1;                          \
> > +     long __fc_mid_x, __fc_x = (x);                                  \
> > +     long __fc_left, __fc_right;                                     \
> > +     typeof(*a) const *__fc_a = (a);                                 \
> > +     for (__fc_i = 0; __fc_i < __fc_as; __fc_i++) {                  \
> > +             __fc_mid_x = (__fc_a[__fc_i] + __fc_a[__fc_i + 1]) / 2; \
> > +             if (__fc_x <= __fc_mid_x) {                             \
> > +                     __fc_left = __fc_x - __fc_a[__fc_i];            \
> > +                     __fc_right = __fc_a[__fc_i + 1] - __fc_x;       \
> > +                     if (__fc_right < __fc_left)                     \
> > +                             __fc_i++;                               \
> > +                     break;                                          \
> > +             }                                                       \
> > +     }                                                               \
> > +     (__fc_i);                                                       \
> > +})
> >
> > ...
> >
> > +#define find_closest_descending(x, a, as)                            \
>
> Boy these things are hard to read.  They're also bloaty and I'm
> counting 36ish callsites!
>

Yeah, they're not easy on the eyes at first.
But you do get used to them, after spending some time trying to
understand why they're not working for some values.

> Can we fix both issues by just giving up on the macro approach and
> reimplement them in out-of-line C code?  All the sites I looked at are
> using 32-bit quantities - a mix of signed and unsigned.
>

Converting this to a static-inline was my other thought, rather than
keeping the macros.
But I'm not sure where to draw the line between too much rework vs a bug-fix.
Just fixing the bug was done in V1 of this patch, but then the kunit
exposed a bunch more.

> It's separate from this bugfix of course, but would it be feasible for
> someone to go switch all callers to use u32's then reimplement these in
> lib/find_closest.c?
>

That would work.
How would a rework be preferred?
As a continuation to this patchset? Or a V3 to this patchset?

It's not a big effort to do, now that the kunit is in place.
I actually have a bunch of kunit variants (locally) that try various
combinations of signed/unsigned X & arrays.
But they drove me slightly nuts, until I decided to do the enforcement
to 'long' type for x, mid, left, right variables.

A catch-all implementation (for all current use-cases) would be best
with an int32 vs uint32 for X, mid, left & right (variables).
The thing with X being an int32 is more related to what userspace
would expect to see when inputting a negative number against an array
(of signed or unsigned).
The type of the elements in the array (signed or unsigned) doesn't
matter much (if focusing on the type for the X, mid, left & right
variables).

For the oversampling feature in ad7606, with unsigned X:
   echo -1 > /sys/bus/iio/devices/iio\:device0/oversampling_ratio
would return 128 rather than 0 (for "cat
/sys/bus/iio/devices/iio\:device0/oversampling_ratio")
Right now, the IIO framework treats -1 as an 'int'

But, moving forward: what would some preferences be?
- have variants of find_closest() for unsigned/signed arrays? (
find_closest_u32() or find_closest_i32() ?)
   - AFAICT so far, there aren't any values in the arrays that get
close to INT32_MAX, so int32 may work for now
   - maybe later some 64-bit variants could be added if needed
- should the variables X, mid, left & right be the same signedness as the array

The only preference (towards which I'm leaning) is just making sure
that X (and friends) are signed.
Andrew Morton Nov. 6, 2024, 8:08 p.m. UTC | #3
On Wed, 6 Nov 2024 16:03:36 +0200 Alexandru Ardelean <aardelean@baylibre.com> wrote:

> On Wed, Nov 6, 2024 at 1:08 AM Andrew Morton <akpm@linux-foundation.org> wrote:
> > Can we fix both issues by just giving up on the macro approach and
> > reimplement them in out-of-line C code?  All the sites I looked at are
> > using 32-bit quantities - a mix of signed and unsigned.
> >
> 
> Converting this to a static-inline was my other thought, rather than
> keeping the macros.

Non-inline, I think.  It's big.

> But I'm not sure where to draw the line between too much rework vs a bug-fix.
> Just fixing the bug was done in V1 of this patch, but then the kunit
> exposed a bunch more.

Sure, just the minimum for a bugfix.

> > It's separate from this bugfix of course, but would it be feasible for
> > someone to go switch all callers to use u32's then reimplement these in
> > lib/find_closest.c?
> >
> 
> That would work.
> How would a rework be preferred?
> As a continuation to this patchset? Or a V3 to this patchset?

A new and separate patchset.  A low-priority cleanup from whoever has
the time and motivation ;)

> But, moving forward: what would some preferences be?
> - have variants of find_closest() for unsigned/signed arrays? (
> find_closest_u32() or find_closest_i32() ?)
>    - AFAICT so far, there aren't any values in the arrays that get
> close to INT32_MAX, so int32 may work for now
>    - maybe later some 64-bit variants could be added if needed
> - should the variables X, mid, left & right be the same signedness as the array
>
> The only preference (towards which I'm leaning) is just making sure
> that X (and friends) are signed.

Yes, I guess int32 would be best.  I agree that unsigned values greater
than INT_MAX are unlikely.

I suggest a series of patches which convert individual callers to int32
and the final patch introduces lib/find_closest.c.
diff mbox series

Patch

diff --git a/include/linux/util_macros.h b/include/linux/util_macros.h
index 6bb460c3e818..825487fb66fa 100644
--- a/include/linux/util_macros.h
+++ b/include/linux/util_macros.h
@@ -4,19 +4,6 @@ 
 
 #include <linux/math.h>
 
-#define __find_closest(x, a, as, op)					\
-({									\
-	typeof(as) __fc_i, __fc_as = (as) - 1;				\
-	typeof(x) __fc_x = (x);						\
-	typeof(*a) const *__fc_a = (a);					\
-	for (__fc_i = 0; __fc_i < __fc_as; __fc_i++) {			\
-		if (__fc_x op DIV_ROUND_CLOSEST(__fc_a[__fc_i] +	\
-						__fc_a[__fc_i + 1], 2))	\
-			break;						\
-	}								\
-	(__fc_i);							\
-})
-
 /**
  * find_closest - locate the closest element in a sorted array
  * @x: The reference value.
@@ -25,8 +12,27 @@ 
  * @as: Size of 'a'.
  *
  * Returns the index of the element closest to 'x'.
+ * Note: If using an array of negative numbers (or mixed positive numbers),
+ *       then be sure that 'x' is of a signed-type to get good results.
  */
-#define find_closest(x, a, as) __find_closest(x, a, as, <=)
+#define find_closest(x, a, as)						\
+({									\
+	typeof(as) __fc_i, __fc_as = (as) - 1;				\
+	long __fc_mid_x, __fc_x = (x);					\
+	long __fc_left, __fc_right;					\
+	typeof(*a) const *__fc_a = (a);					\
+	for (__fc_i = 0; __fc_i < __fc_as; __fc_i++) {			\
+		__fc_mid_x = (__fc_a[__fc_i] + __fc_a[__fc_i + 1]) / 2;	\
+		if (__fc_x <= __fc_mid_x) {				\
+			__fc_left = __fc_x - __fc_a[__fc_i];		\
+			__fc_right = __fc_a[__fc_i + 1] - __fc_x;	\
+			if (__fc_right < __fc_left)			\
+				__fc_i++;				\
+			break;						\
+		}							\
+	}								\
+	(__fc_i);							\
+})
 
 /**
  * find_closest_descending - locate the closest element in a sorted array
@@ -36,9 +42,27 @@ 
  * @as: Size of 'a'.
  *
  * Similar to find_closest() but 'a' is expected to be sorted in descending
- * order.
+ * order. The iteration is done in reverse order, so that the comparison
+ * of '__fc_right' & '__fc_left' also works for unsigned numbers.
  */
-#define find_closest_descending(x, a, as) __find_closest(x, a, as, >=)
+#define find_closest_descending(x, a, as)				\
+({									\
+	typeof(as) __fc_i, __fc_as = (as) - 1;				\
+	long __fc_mid_x, __fc_x = (x);					\
+	long __fc_left, __fc_right;					\
+	typeof(*a) const *__fc_a = (a);					\
+	for (__fc_i = __fc_as; __fc_i >= 1; __fc_i--) {			\
+		__fc_mid_x = (__fc_a[__fc_i] + __fc_a[__fc_i - 1]) / 2;	\
+		if (__fc_x <= __fc_mid_x) {				\
+			__fc_left = __fc_x - __fc_a[__fc_i];		\
+			__fc_right = __fc_a[__fc_i - 1] - __fc_x;	\
+			if (__fc_right < __fc_left)			\
+				__fc_i--;				\
+			break;						\
+		}							\
+	}								\
+	(__fc_i);							\
+})
 
 /**
  * is_insidevar - check if the @ptr points inside the @var memory range.