Skip to content

sys/phydat: New phydat_fit API#9702

Merged
miri64 merged 1 commit intoRIOT-OS:masterfrom
maribu:phydat_fit
Oct 25, 2018
Merged

sys/phydat: New phydat_fit API#9702
miri64 merged 1 commit intoRIOT-OS:masterfrom
maribu:phydat_fit

Conversation

@maribu
Copy link
Member

@maribu maribu commented Aug 3, 2018

The current phydat_fit implementation has two limitations: First, the API is way more complicated to use than needed. Second, it doesn't perform correct rounding. This PR addresses both.

Before this commit code using it looked like this:

long values[] = { 100000, 2000000, 30000000 };
phydat_t dat = { .scale = 42, .unit = UNIT_V };
phydat_fit(&dat, values[0], 0, phydat_fit(&dat, values[1], 1, phydat_fit(&dat, values[2], 2, 0)));

Now it can be used like this:

long values[] = { 100000, 2000000, 30000000 };
phydat_t dat = { .unit = UNIT_V, .scale = 42 };
phydat_fit(&dat, values, 3);

@maribu
Copy link
Member Author

maribu commented Aug 3, 2018

BTW: With k dimensions and n changes in scale, the old version required O(k*n) divisions. The new versions requires O(k+n) divisions and O(n) multiplications. Even though this won't matter in practice, it still reads nice :-)

@miri64 miri64 requested a review from jnohlgard August 3, 2018 16:41
@miri64 miri64 added the Process: API change Integration Process: PR contains or issue proposes an API change. Should be handled with care. label Aug 3, 2018
@maribu
Copy link
Member Author

maribu commented Aug 4, 2018

OK, I was wrong that there is no obvious way of how to round negative numbers. This PR implements rounding away from zero. E.g. 0.5 ~= 1 and -0.5 ~= -1. This has the advantage that if |x| = |y| is true before rounding, it is true after rounding. But it seems that there are quite a lot of opinions how rounding is correctly performed...

@maribu
Copy link
Member Author

maribu commented Aug 6, 2018

I just updated the documentation to explicitly state the rounding strategy, so that people expecting other rounding strategies are at least warned.

Update: I fixed the whitespace error I just introduced and squashed

@maribu
Copy link
Member Author

maribu commented Aug 6, 2018

Just for reference the most popular rounding strategies with some examples:

Original Value Always Down Always Up Towards Infinity Towards Zero Next Even Next Odd
0.4 0 0 0 0 0 0
0.6 1 1 1 1 1 1
0.5 0 1 1 0 0 1
1.5 1 2 2 1 2 1
-0.5 -1 0 -1 0 0 -1
-1.5 -2 -1 -2 -1 -2 -1

So the strategies only differ when the result is exactly between two integer values.

@maribu maribu force-pushed the phydat_fit branch 3 times, most recently from 33217d0 to c3a8ed2 Compare August 6, 2018 17:17
@maribu
Copy link
Member Author

maribu commented Aug 6, 2018

Sorry, one more change: I change the input type from long to in32_t. I think in this API it is crucial to know the actual range the input type supports. Also it is more consistent with phydat_t, which uses int16_t instead of short for (I believe) the very same reason.

@maribu
Copy link
Member Author

maribu commented Aug 6, 2018

  • Fixed wrong variable name in assert
  • Squashed commits

Btw: Maybe assertions could be turned on by default in the unit tests?

@maribu maribu force-pushed the phydat_fit branch 2 times, most recently from 96205db to 7bd6278 Compare August 6, 2018 21:26
@maribu
Copy link
Member Author

maribu commented Aug 15, 2018

@gebart: ping :-)

@maribu
Copy link
Member Author

maribu commented Aug 22, 2018

Ping?

@maribu
Copy link
Member Author

maribu commented Sep 19, 2018

@miri64: @gebart seems to be to busy to review this PR. Do you have someone else in mind who could review this? I want to use this API in another PR. Therefore that PR is put on hold until this PR is decided on.

@miri64
Copy link
Member

miri64 commented Sep 19, 2018

In general the change looks sensible to me. Maybe @haukepetersen can have a look. However, @gebart originally provided this function, so if possible I'd like to have his opinion on this change as well (that's why I originally asked for his review).

Copy link
Member

@jnohlgard jnohlgard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an awful lot of division going on here, but I guess the original implementation wasn't very optimized either.
See my comments inline. The behaviour seems broken for -32768 and +32767, as they are supposed to fit inside an int16_t.

* [@ref PHYDAT_MIN, @ref PHYDAT_MAX], and updates the stored scale factor.
* The value is rounded towards infinity, e.g. `0.5` and `0.6` are rounded to
* `1`, `0.4` and `-0.4` are rounded to `0`, `-0.5` and `-0.6` are rounded to
* `-1`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Towards infinity (+infinity) means that 0.1 would be rounded to 1, -0.9 would be rounded to 0. This is rounding towards nearest integer (round half away from zero).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point, this name is indeed misleading. Wikipedia refers to it as round half away from zero in addition to "towards infinity". Typing it into some internet search engine also shows that "rounding away from zero" is the more widely used term then "towards infinity".

{ .val = { 0, 0, 12346 }, .unit = UNIT_M, .scale = 7 },
{ .val = { 3277, 3277, 3277 }, .unit = UNIT_NONE, .scale = 1 },
{ .val = { 32766, 32766, 32766 }, .unit = UNIT_NONE, .scale = 0 },
{ .val = { -3277, -3277, -3277 }, .unit = UNIT_NONE, .scale = 1 },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong behaviour IMO.
-32767 fits inside phydat_t::dat

Also, please add tests for -32768 (expect -32768), -32769 (expect -3277, scale +1), 32767 (expect 32767), 32768 (expect 3277, scale 1)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was actually a deliberate implementation decision for two reasons:

  • Both positive and negative numbers can be treated identical if treating -32768 as out of range
  • Special handling of the scale estimation can be skipped if treating 32767 (and thus also -32767) as out of range.

The special case for scale estimation is when the number is 32767 and has to be rounded up. By treating only [-32766, 32766] as target range no special treatment is required, which makes the code shorter, the ROM smaller and faster. Also, in 99.99542% of random numbers the result is the some, in the remaining 0.00458% of the cases some precision is lost.

But let me think about it. Maybe there is an elegant solution to use the full range.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good arguments. It makes sense to introduce this limitation to save on code size and simplify the implementation if a rationale is clearly described in the module documentation.

@jnohlgard
Copy link
Member

I agree the API usage was a bit hard to read before. This seem better in that regard.

Copy link
Member

@jnohlgard jnohlgard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to set request changes.

@maribu
Copy link
Member Author

maribu commented Sep 21, 2018

@gebart: I basically rewrote the phydat_fit function now to use lookup tables. Apart from speeding things up quite a bit by significantly reducing the number of divisions required, this also allows to take rounding into account when detecting the correct scale. Thus, 32767 and -32767 can now be used. Still, by default, -32768 is considered out of range to safe ROM for a separate lookup table just for negative numbers. The probability of precision loss is reduced down to 0.00153% compared to the original implementation this way.

Additionally, by adding -DPHYDAT_FIT_TRADE_PRECISION_FOR_ROM=0 to the compiler flags, the separate lookup table is used for negative numbers to use the full range of phydat_t. This costs 20 bytes for the lookup table and two assignments (I guess about 28 bytes in total depending on the instruction set used).

@maribu
Copy link
Member Author

maribu commented Oct 2, 2018

@gebart: Could you please re-review? Thanks :-)

Copy link
Member

@jnohlgard jnohlgard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change looks good. Tested working on native and frdm-kw41z.

This small diff saves a few hundred bytes of ROM on armv6 by using unsigned division instead of signed.

diff --git a/sys/phydat/phydat.c b/sys/phydat/phydat.c
index 245d8f89d9..56b37d7d59 100644
--- a/sys/phydat/phydat.c
+++ b/sys/phydat/phydat.c
@@ -48,7 +48,7 @@ static const int32_t lookup_table_negative[] = {
 };
 #endif
 
-static const int32_t divisors[] = {
+static const uint32_t divisors[] = {
     100000,
     10000,
     1000,
@@ -61,7 +61,7 @@ static const int32_t divisors[] = {
 void phydat_fit(phydat_t *dat, const int32_t *values, unsigned int dim)
 {
     assert(dim <= (sizeof(dat->val) / sizeof(dat->val[0])));
-    int32_t divisor = 0;
+    uint32_t divisor = 0;
     int32_t max = 0;
     const int32_t *lookup = lookup_table_positive;
 
@@ -104,17 +104,17 @@ void phydat_fit(phydat_t *dat, const int32_t *values, unsigned int dim)
     }
 
     /* Applying scale and add half of the divisor for correct rounding */
-    long divisor_half = divisor >> 1;
+    uint32_t divisor_half = divisor >> 1;
     for (unsigned int i = 0; i < dim; i++) {
         if (values[i] >= 0) {
-            dat->val[i] = (values[i] + divisor_half) / divisor;
+            dat->val[i] = (uint32_t)(values[i] + divisor_half) / divisor;
         }
         else {
             /* For negative integers the C standards seems to lack information
              * on whether to round down or towards zero. So using positive
              * integer division as last resort here.
              */
-            dat->val[i] = -(((-values[i]) + divisor_half) / divisor);
+            dat->val[i] = -((uint32_t)((-values[i]) + divisor_half) / divisor);
         }
     }
 }

* @param[in] value value to rescale
* @param[in] index place the value at this position in the phydat_t::val array
* @param[in] prescale start by scaling the value by this exponent
* @warning The scale member in @p dat has to be initialized by the caller prior
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use @pre for documenting preconditions and assumptions regarding parameters.
Optional extra: I think doxygen will generate a correct hyperlink if you write @ref phydat_t::scale instead of just scale

* be used for both positive and negative values. As result, -32768 will be
* considered as out of range and scaled down. So statistically in 0.00153%
* of the cases an unneeded scaling is performed, when
* PHYDAT_FIT_TRADE_PRECISION_FOR_ROM is true.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very helpful comment!

10,
};

#define LOOKUP_LEN (sizeof(lookup_table_positive)/sizeof(int32_t))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add spaces around the division operator. uncrustify will complain otherwise.

dat->val[k] /= 10;

for (unsigned int i = 0; i < LOOKUP_LEN; i++) {
if (max > lookup[i]){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

space before {

}

/* Applying scale and add half of the divisor for correct rounding */
long divisor_half = divisor >> 1;
Copy link
Member

@jnohlgard jnohlgard Oct 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

int32_t uint32_t

long divisor_half = divisor >> 1;
for (unsigned int i = 0; i < dim; i++) {
if (values[i] >= 0) {
dat->val[i] = (values[i] + divisor_half) / divisor;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if this pulls in signed or unsigned division helpers on platforms without hardware division?
The preconditions before ensures that both operands are positive, but they are still signed types, so it is likely that this calls signed division (__divsi3, __aeabi_idiv). Unsigned division is cheaper in terms of both ROM usage and CPU cycles, at least on ARMv6, but I'm guessing it should be similar on smaller arches too e.g. AVR (for reference: #9738)

@jnohlgard jnohlgard added CI: ready for build If set, CI server will compile all applications for all available boards for the labeled PR Area: SAUL Area: Sensor/Actuator Uber Layer Type: enhancement The issue suggests enhanceable parts / The PR enhances parts of the codebase / documentation labels Oct 2, 2018
@jnohlgard jnohlgard added this to the Release 2018.10 milestone Oct 2, 2018
@jnohlgard jnohlgard self-assigned this Oct 2, 2018
@maribu
Copy link
Member Author

maribu commented Oct 2, 2018

@gebart: Thanks for your review. I was completely unaware that signed division could be more expensive. Thanks for pointing that out :-)

@maribu
Copy link
Member Author

maribu commented Oct 8, 2018

The failed Murdock build seems to be unrelated. Let me try if it works after a rebase.

@maribu
Copy link
Member Author

maribu commented Oct 10, 2018

@gebart: The rebase solved indeed the Murdock issue :-). Could you check if I addressed your comments adequately?

@miri64 miri64 added CI: needs squashing Commits in this PR need to be squashed; If set, CI systems will mark this PR as unmergable CI: needs squashing and removed CI: needs squashing labels Oct 10, 2018
@maribu
Copy link
Member Author

maribu commented Oct 24, 2018

@gebart: Sorry to annoy you again. Could you please check if I addressed your comments? If so, I would squash and rebase against the current master.

Copy link
Member

@jnohlgard jnohlgard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all of my comments were addressed.
Please squash

The current phydat_fit implementation the following limitations:
- The API is way more complicated to use than needed
- It doesn't perform any rounding
- It uses `long` in a place where actual width (or better range) of the type
  is pretty important.

This commit addresses these limitations and uses lookup-tables to reduce the
number of divisions required.

Before this commit code using it looked like this:
``` C
long values[] = { 100000, 2000000, 30000000 };
phydat_t dat = { .scale = 42, .unit = UNIT_V };
phydat_fit(&dat, values[0], 0, phydat_fit(&dat, values[1], 1, phydat_fit(&dat, values[2], 2, 0)));
```

Now it can be used like this:
``` C
int32_t values[] = { 100000, 2000000, 30000000 };
phydat_t dat = { .unit = UNIT_V, .scale = 42 };
phydat_fit(&dat, values, 3);
```
@miri64 miri64 added CI: ready for build If set, CI server will compile all applications for all available boards for the labeled PR and removed CI: needs squashing Commits in this PR need to be squashed; If set, CI systems will mark this PR as unmergable CI: ready for build If set, CI server will compile all applications for all available boards for the labeled PR labels Oct 24, 2018
@maribu
Copy link
Member Author

maribu commented Oct 25, 2018

Squashed and rebased :-)

@gebart: Thanks for your review and for pointing out the different ROM size of signed and unsigned divisions.

@miri64
Copy link
Member

miri64 commented Oct 25, 2018

Since this is an API change I don't want to merge this in hour zero of the feature freeze. Let's wait a bit, until the release branch is open.

@miri64
Copy link
Member

miri64 commented Oct 25, 2018

Let's wait a bit, until the release branch is open.

It is. Let's go!

@miri64 miri64 merged commit 959e449 into RIOT-OS:master Oct 25, 2018
@miri64
Copy link
Member

miri64 commented Oct 25, 2018

Thanks for your contribution and your patience @maribu!

@maribu maribu deleted the phydat_fit branch October 25, 2018 18:11
@maribu
Copy link
Member Author

maribu commented Oct 25, 2018

No problem. I learned something really useful, so it was well spent time :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: SAUL Area: Sensor/Actuator Uber Layer CI: ready for build If set, CI server will compile all applications for all available boards for the labeled PR Process: API change Integration Process: PR contains or issue proposes an API change. Should be handled with care. Type: enhancement The issue suggests enhanceable parts / The PR enhances parts of the codebase / documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants