The order of operations matters. A lot!

General discussions and questions abound development of code with MicroPython that is not hardware specific.
Target audience: MicroPython Users.
martincho
Posts: 96
Joined: Mon May 16, 2022 9:59 pm

Re: The order of operations matters. A lot!

Post by martincho » Thu Jun 16, 2022 3:39 pm

Ok, but the counting down compares using equality, the other one using less than.
It's a good question. The kind you have to ask when you are looking to optimize things. And testing is key, always.

I ran the test with both options by commenting out one or the other.

Code: Select all

@micropython.native
def counting_up():
    i = 0
    limit = 5000
    busy = False
    # while i < limit:
    while i != limit:
        i += 1
        busy = True
    busy = False
    return [busy, i]
The result for using "while i != limit":

Code: Select all

Total run time for 100 iterations:
counting_up:                926.0 ms for 100 iterations, output:[False, 5000]
counting_down:              518.0 ms for 100 iterations, output:[False, 0]
using_range_from_zero:     1206.0 ms for 100 iterations, output:[False, 5000]
using_range_from_value:    1214.0 ms for 100 iterations, output:[False, 0]
For the "while i < limit" case:

Code: Select all

Total run time for 100 iterations:
counting_up:                794.0 ms for 100 iterations, output:[False, 5000]
counting_down:              518.0 ms for 100 iterations, output:[False, 0]
using_range_from_zero:     1206.0 ms for 100 iterations, output:[False, 5000]
using_range_from_value:    1214.0 ms for 100 iterations, output:[False, 0]
In this case, using "while i != limit" is 16% slower than using "while i < limit". In the first case, the countdown version is nearly twice as fast!
That's not the same as comparing wheher a == 0 is faster than a == 23 which is what we were discussing originally. I don't have time to write a complete portable script like yours, sorry, but for fun this compares 4 cases:
I think this is where designing the test can be super important. And, yes, it takes a lot more thought than either one of us can devote to this at the moment. Even the tests I concocted are likely flawed on more ways than one.

I think the reason you are seeing the results you got is because the loops are not doing much. The variables are being allocated on the stack and very likely stay in registers for the entire run. If the variables stay in registers, execution will be very fast --almost regardless of the kind of comparison you might use.

You could, for example, try with a 32 bit number rather than an 8 bit number that is super-fast to load. Something like 0x12345678. That's one area where a countdown can take a serious hit. When I write code in assembler and have no choice but to compare to a number, I do my best to try to load that number into a register and never alter that register until I am done with the number. In that case, depending on the processor, there is no performance hit between counting up and down. Also, return the values and print them at the end (not for every loop). This is to force a situation where they are deemed important (as they might be in a real application) and not optimized away or otherwise discarded.

In other words, the question you are trying to answer is "How do these approaches differ in the context of of my real application?" as opposed to a laboratory experiment. It's like simulations; the only people who believe simulation results are the folks who wrote the simulation code.

Code: Select all

Idea is to test bare oprations and not much more
As mentioned, you have to be careful when designing the test. Experience in assembler or C can help. This is why I almost never run pure single operation tests. They don't necessarily represent what will happen when running real code.

I find it perfectly understandable that if one does not have one million devices, and might even have a one-shot script, one chooses range() because it simple, clear, just works, known by everybody, requires no tests, and so on.
Absolutely agree on that one. Even if you are doing commercial work, do not ever optimize unless absolutely necessary. Or, at the very least, at the end of the process. Optimizing early is a complete waste of time. So, yeah, use range() and be super-Pythonic while prototyping. Worry about optimization later.

The project I am working on will be re-written entirely in C. MicroPython was thought to be a faster way to get to a result for some testing. The theory was that you could write the entire application much faster, validate algorithms and ideas and then optimize by re-writing it in C. Once you know what you need to do, re-coding something in C isn't difficult at all. This, I think, is particularly true in embedded systems, where knowing what data you have to manage in memory and how can help you design a memory map for best efficiency.

Sadly, I have to comment, this was a mistake. The performance issues with MicroPython led to having to dive into early optimization and even writing a bunch of assembler code. When that happened, the development speed advantage of Python was absolutely gone. Had I known what I know now (this is my first time using MicroPython) I would have gone straight for C from the start. I would have been much farther along today had I taken that path. That said, MicroPython is a wonderful tool and it has many uses, even serious production applications --it just didn't fit what I happen to be doing.

User avatar
scruss
Posts: 360
Joined: Sat Aug 12, 2017 2:27 pm
Location: Toronto, Canada
Contact:

Re: The order of operations matters. A lot!

Post by scruss » Thu Jun 16, 2022 7:43 pm

range() does a whole lot more that being a loop counter: it's a whole Python type of its own, representing an immutable sequence of numbers. You can do things like:

Code: Select all

>>> r=range(20,-1,-2)
>>> type(r)
<class 'range'>
>>> r
range(20, -1, -2)
>>> list(r)
[20, 18, 16, 14, 12, 10, 8, 6, 4, 2, 0]
>>> 10 in r          # set-like membership testing
True
>>> 11 in r          # ... not just testing if numbers are inside start-end range, either
False
>>> r[3:6]          # sub-ranges
range(14, 8, -2)
>>> list(r[3:6])
[14, 12, 10]
So of course it's slower. It's doing a lot more, and you can do a lot more with it.

I'm not involved in large-series production, so I'm not convinced by your hard gospel requirement to use the most energy efficient programming language. My development computer draws about 90 W. Absolutely flat out, a Raspberry Pi Pico could draw 0.475 W (from datasheet, p.11). So if I spend an extra hour writing code in a more efficient language on my development computer, that 90 Wh represents about 8 days of run time for the Raspberry Pi Pico. I'd be far better off reducing the development time than the run efficiency.

Sorry that MicroPython didn't work for you, but thanks for giving it a shot. We get a lot of "MiCrOpYtHoN iS sLoW!!!!" threads here that typically go nowhere, but yours were thoughtful.

martincho
Posts: 96
Joined: Mon May 16, 2022 9:59 pm

Re: The order of operations matters. A lot!

Post by martincho » Fri Jun 17, 2022 1:02 am

Oh, please, don't get me wrong. I am not complaining at all. Just stating a reality, that's all.

I will definitely continue to use MicroPython. It's a great tool. Without a doubt. And it's getting better with every release.

One of the things I intend to do on the other side of this project that has me coding 18 hours/day 7 days/week (hopefully done in a couple of weeks) will be to get a much better handle of making effective use of Viper. I definitely cornered myself into a situation where I have a ton of code that would have to be ripped-up and rewritten in order to make use of it. This was purely out of ignorance on my part. I jumped in and went for it. Didn't even know about Viper until I was forced to make things go faster. First, a vacation though.

While I enjoy coding in assembly language on any processor, development and debugging is 10x slower, if not worse. Viper is an amazing tool that can deliver massive performance gains. The thing is, you have to know how to code for it.

The other thing I want to do is contribute in whatever ways I may be able to. I have lot of questions, none of which I will be able to answer until I can take the time to dive into MicroPython source code and understand what's going on under the hood.

Post Reply