Skip to content

sys/phydat: New phydat_fit API#9702

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

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

Conversation

@maribu

@maribu maribu commented Aug 3, 2018

Copy link
Copy Markdown
Member

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

maribu commented Aug 3, 2018

Copy link
Copy Markdown
Member Author

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

maribu commented Aug 4, 2018

Copy link
Copy Markdown
Member Author

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

maribu commented Aug 6, 2018

Copy link
Copy Markdown
Member Author

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

maribu commented Aug 6, 2018

Copy link
Copy Markdown
Member Author

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

maribu commented Aug 6, 2018

Copy link
Copy Markdown
Member Author

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

maribu commented Aug 6, 2018

Copy link
Copy Markdown
Member Author
  • 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

maribu commented Aug 15, 2018

Copy link
Copy Markdown
Member Author

@gebart: ping :-)

@maribu

maribu commented Aug 22, 2018

Copy link
Copy Markdown
Member Author

Ping?

@maribu

maribu commented Sep 19, 2018

Copy link
Copy Markdown
Member Author

@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

miri64 commented Sep 19, 2018

Copy link
Copy Markdown
Member

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).

@jnohlgard jnohlgard left a comment

Copy link
Copy Markdown
Member

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.

Comment thread sys/include/phydat.h Outdated
* [@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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Member

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

@jnohlgard jnohlgard left a comment

Copy link
Copy Markdown
Member

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

maribu commented Sep 21, 2018

Copy link
Copy Markdown
Member Author

@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

maribu commented Oct 2, 2018

Copy link
Copy Markdown
Member Author

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

@jnohlgard jnohlgard left a comment

Copy link
Copy Markdown
Member

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);
         }
     }
 }

Comment thread sys/include/phydat.h Outdated
* @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
Copy Markdown
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

Comment thread sys/phydat/phydat.c
* 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
Copy Markdown
Member

Choose a reason for hiding this comment

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

Very helpful comment!

Comment thread sys/phydat/phydat.c Outdated
10,
};

#define LOOKUP_LEN (sizeof(lookup_table_positive)/sizeof(int32_t))

Copy link
Copy Markdown
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.

Comment thread sys/phydat/phydat.c Outdated
dat->val[k] /= 10;

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

space before {

Comment thread sys/phydat/phydat.c Outdated
}

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

@jnohlgard jnohlgard Oct 2, 2018

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

int32_t uint32_t

Comment thread sys/phydat/phydat.c Outdated
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
Copy Markdown
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)

Comment thread tests/unittests/tests-phydat/tests-phydat.c
@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

maribu commented Oct 2, 2018

Copy link
Copy Markdown
Member Author

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

@maribu

maribu commented Oct 8, 2018

Copy link
Copy Markdown
Member Author

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

@maribu

maribu commented Oct 10, 2018

Copy link
Copy Markdown
Member Author

@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

maribu commented Oct 24, 2018

Copy link
Copy Markdown
Member Author

@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.

@jnohlgard jnohlgard left a comment

Copy link
Copy Markdown
Member

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

maribu commented Oct 25, 2018

Copy link
Copy Markdown
Member Author

Squashed and rebased :-)

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

@miri64

miri64 commented Oct 25, 2018

Copy link
Copy Markdown
Member

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

miri64 commented Oct 25, 2018

Copy link
Copy Markdown
Member

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

miri64 commented Oct 25, 2018

Copy link
Copy Markdown
Member

Thanks for your contribution and your patience @maribu!

@maribu maribu deleted the phydat_fit branch October 25, 2018 18:11
@maribu

maribu commented Oct 25, 2018

Copy link
Copy Markdown
Member Author

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