-
Notifications
You must be signed in to change notification settings - Fork 524
Teal4 math #2139
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Teal4 math #2139
Conversation
b12c44c to
64df154
Compare
data/transactions/logic/doc.go
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not have b~ for bitwise invert as well?
Is it because byteslices are variable length and the normal invert is kind of undefined in this scenario?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because it's confusing. If you start with 0xff00 and invert it, you get 0xff (note, only one byte). So then if you invert it again, you get "" (empty byteslice == 0). The results of these things are always only as long as needed to represent the answer as a bigint. So I think we instead have to recommend people use a bunch of 1s, and xor, to get the functional equivalent of bit negation. That way their choice of length for the 1s slice explicit forces the final result to be the same length.
I'm going to add an opcode to make such 1-strings easy to build.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Saying all this outloud led to a better way. bitwise operation no longer truncate to the shortest byteslice that can hold the value. They return a byteslice as long as their longest input. This is more likely to be what people want, as they would use these operations when treating the slices as arrays of bits. It also makes the definition of b~ more straightforward.
This allows callers to do less work. The consensus protocol was already being supplied, so Check() has everything it needed to do the right thing. This also consolidates the code that will act differently for dynamic teal costs.
algorandskiy
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work, couple comments
|
|
||
| if cx.err == nil { | ||
| postheight := len(cx.stack) | ||
| if spec.Name != "return" && postheight-preheight != len(spec.Returns)-len(spec.Args) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the check is correct but the message would be -2 != -1 or similar that might be confusing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This error would only occur because of a bug in our implementation, not in teal code. I don't know how user-friendly it needs to be. The info we need is there: There's a bug in the named opcode and it didn't manage the stack properly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I simply do not remember checks for popped vs pushed. But I guess you are right and these problem would be already discovered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's true, those checks are new. But they are checks on the results of individual opcode operations, so I don't think it is possible to construct a strange program that would trip them. If they catch a problem, it would mean that one of our existing opcodes has a pretty important bug that we have missed for a long time. I think that's unlikely. But it does help mistakes as we write new opcodes.
| preheight := len(cx.stack) | ||
| spec.op(cx) | ||
|
|
||
| if cx.err == nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this needs to go under consensus params switch, since there is a change upgraded nodes reject some specially-crafted program, and all allow it => fork
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so. It only checks things that would have previously caused an error. But now it does it in a central place, so that future opcodes won't have to do it themselves. In particular, in the past only concat could create a 4k slice, so only concat checked its output. Now we check here, in case we have introduced a new way to create a 4k slice (I don't think we have, since byteslice math is input limited, but this seems like a useful thing to centralize, especially as we might add other ways to hit 4k)
| opSwap(cx) | ||
| opLt(cx) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought entire point of duplicating code in each opcode was in reducing number of additional operations.
Like opLe now is opSwap + opLt + opNot that is (5 + 6 + 3) = 14 instead of 6 in old version (counted mem read/write, swap is expensive since no direct mem to mem copy in a single cpu operation).
IMO this is questionable, maybe benchmark it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did this when I found writing the equivalent byteslice operators to be tedious, repetitive, and error-prone. It's hard for me to imagine any appreciable performance effect here, with all this data in cache. I'll write a little benchmark to convince myself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here are the result. I put them in order of overhead introduced by the shorter, clearer code. Each one is on more function call than the last.
- Benchmark < 16.9-17.8 ns/op
- Benchmark >= 17.3-17.5 ns/op
- Benchmark > 20.0-23.3 ns/op
- Benchmark <= 21.8-22.0 ns/op
3 ns seems like an ok price to pay, for the largest overhead.
The biggest jump is between 2 & 3, because it involves opSwap. I suppose I could be convinced to repeat the code to eliminate that one. Then they'd all be so close the difference is in the noise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for benchmarking it! I think I agree, 3ns/op is OK price for cleaner code.
Extrapolate: suppose there are 300 (1/3 of program) of comparison instructions then there is 300*3ns =~ 1 us overhead. For 10k programs in a block we'll have 10ms execution time increase. I guess this is acceptable.
data/transactions/logic/eval_test.go
Outdated
| testAccepts(t, "int 2; log2; int 2; ==", 4) | ||
| testAccepts(t, "int 4; log2; int 3; ==", 4) | ||
| testAccepts(t, "int 5; log2; int 3; ==", 4) | ||
| testAccepts(t, "int 8; log2; int 4; ==", 4) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I though log2(8) = 3
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yikes, yes, thank you! Off by one everywhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason I made this error is that integer log2 is just "what's the highest bit set in this integer (minus 1)".
I changed this so it's "correct", and fails when the supplied integer is 0. But I wonder if I should just rename it "highbit" and give it the old semantics. That sounds clearer to me, and is probably what people would actually need this opcode for (so they can decide how far to bitshift a value, for example). That's why this is an operation that's supported by the bits package. With the log2 semantics, callers need to check if the value is 0 first, to avoid panics.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I decided to call the op "bitlen" and define it on uints and byte-arrays. It returns the index of the highest bit, so it returns 0 on 0, rather than panic. (And 1 => 1, 2 => 2, 4=> 3, 8 => 4). It corresponds directly to bits.Len in Go.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good to me, including the teal.tmLanguage.json file.
algorandskiy
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. Please consider adding one more test for all wide opcodes in a sequence to ensure format compatibility - like mulw, addw, divmodw, and something like (A * B / B == A) for mulw + divmodw. A seprate PR is OK.
| result := new(big.Int) | ||
| opBytesBinOp(cx, result, result.Add) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
clever
| a, b := opBytesBinaryLogicPrep(cx) | ||
| for i := range a { | ||
| a[i] = a[i] | b[i] | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
another nice one
|
@brianolson please take a look |
brianolson
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
one tiny testing quibble, but the 0.05% change probably doesn't matter.
Otherwise no objections. LGTM.
| hashes := []string{"sha256", "keccak256", "sha512_256"} | ||
| for _, hash := range hashes { | ||
| b.Run(hash+"-small", func(b *testing.B) { // hash 32 bytes | ||
| benchmarkOperation(b, "byte 0x1234567890", hash, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: for each of these its only hashing 5 bytes on the first execution, and then {32,128,512} for the next 1999. It could be perfectly consistent by using a 32 byte constant and running dup; concat; dup; concat; {hash} as the repeated op?
|
Brian's comments to be addressed in a separate PR. |
Add a lot of math oriented opcodes that have been requested by teal users. sqrt, exp, expw, divmodw byteslice oriented math - a set of opcodes that will treat byteslices as arbitrary precision uints.
Summary
Add a lot of math oriented opcodes that have been requested by teal users.
sqrt,exp,expw,divmodwbyteslice oriented math - a set of opcodes that will treat byteslices as arbitrary precision uints.
Test Plan
Unit tests
Byte math costs were established with a bunch of new operation benchmarks.