All of your if char == '>': ptr += 1
and similar checks should use elif
after the first check. By using if
for all of the checks, you're forcing them all to run, even once you've found a match. This is wasteful because the checks are necessarily exclusive of each other. Once one check is true, none of the other checks can be. For example, you should have:
if char == '>': ptr += 1
elif char == '<': ptr -= 1
elif char == '+': values[ptr] = (values[ptr] + 1) % 256
elif char == '-': values[ptr] = (values[ptr] - 1) % 256
Now the checks stop once a match is found which prevents unnecessary equality checks.
I'd also try to break this down into a few functions to help testing. Right now, you can only test bf_interpreter
as one whole. You could have a function that takes the current character, and the state of the program (the brackets
, ptr
, outputs
...), and returns a new state. That way you can easily test for a given state if a certain command produces a correct new state.
I'm assuming this loop is just to add padding so you don't go off the end of the slots?:
while length <= ptr:
length += 1
values.append(0)
You could make that a little neater by just using math and some concatenation. You could also just get rid of length
and use len(values)
:
needed = ptr - len(values)
values += [0] * needed
ptr - len(values)
calculates how many slots are needed, then [0] * needed
produces that many 0
s, and +=
adds them to values
. If needed
is negative, [0] * needed
will produce []
, and essentially cause no change.
If you want to avoid the temporary list that [0] * needed
creates, you could replace that with:
values += (0 for _ in range(needed))
Now +=
just pulls from a generator that produces values as needed.
And then, just like how you don't need length
, you don't need code_length
either. len(code)
is fine; len
runs in constant time. You don't need to cache it for performance reasons.
Here's some timings to show the difference in runtime this can cause:
import timeit
TEST_CODE = "++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++."
>>> timeit.timeit(lambda: bf_interpreter_orig(TEST_CODE), number=int(2e5)) # Two hundred thousand tests
77.3481031 # Takes 77 seconds
>>> timeit.timeit(lambda: bf_interpreter_len(TEST_CODE), number=int(2e5))
88.93794809999997
Where bf_interpreter_orig
is your original code, and bf_interpreter_len
is your original code but using len
.
Yes, there's a difference. Note though, that's a ~11 second difference across 200,000 calls. That works out to roughly 58 microseconds per call to the interpreting function.
Unless you're calling bf_interpreter
hundreds of thousands of times in a tight loop, the difference is unlikely to matter. This also likely has nothing to do with the fact that you're requesting a length, and more to do with one extra function call. Function calls aren't super fast in Python. Likely any extra call to any function would have similar effects.
parsing rules
. Could you please elaborate? \$\endgroup\$char == '['
, you're supposed to check whether the value at the pointer is zero; if it is, you need to jump to after the corresponding]
. \$\endgroup\$