Skip to content
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

consensus_decode should take &mut D? #1020

Closed
dpc opened this issue May 28, 2022 · 6 comments · Fixed by #1035
Closed

consensus_decode should take &mut D? #1020

dpc opened this issue May 28, 2022 · 6 comments · Fixed by #1035

Comments

@dpc
Copy link
Contributor

dpc commented May 28, 2022

After taking a stab at #1019 , it seems to me that consensus_decode should take d: &mut D and not as it currently does d: D. The reason is - it is basically a wrapper around Read::read which takes &mut self.

Instead of passing &mut d when descending to sub-decoding, the code could just pass d and it would work. Internally it would avoid creating &mut &mut &mut &mut T which seems kind of undesirable, even if compiler probably can get rid of these needless references.

@apoelstra
Copy link
Member

I could be convinced. I dislake &mut D because it's a little ugly/unergonomic and strictly speaking it's less general since &mut D is Read wheneve D is.

But you may be correct that the tendency to create giant &mut &mut ... &mut chains is even worse. In rust-miniscript we were forced to use &mut D in a lot of places where we had recursive calls, because otherwise the compiler would generate an infinite &mut chain.

In summary, I have no strong opinions here.

@dpc
Copy link
Contributor Author

dpc commented May 28, 2022

it's less general since &mut D is Read wheneve D is

Indeed d: D is more general that d : &mut D. But since the trait implementations are always based on Read::read, I would argue that it's too-general, because what we really need is &mut Read in the first place.

@dpc
Copy link
Contributor Author

dpc commented May 28, 2022

Actually, no. Scratch that. Wrong way to think about it.

We need to think separately about trait bond and function signature.

A trait bond D: Read is most general, yes. And we will keep it.

However

fn foo_ref(f: &mut Foo);

is more general from the caller perspective than:

fn foo_owned(f: Foo);

It is sometimes impossible to use (call) foo_owned, while being able to call foo_ref. And it is reversed from the implementation perspective: It is sometimes impossible to implement foo_ref, while it might be possible to implement foo_owned. This is a tradeoff between freedom/flexibility between caller and implementation.

And we know that we only ever going to need &mut D to implement it, so it's best for us to give that freedom back to the caller, because when doing recursive decoding we are also the caller. :D

Because of the current needlessly strict calling convention, we are sometimes unable to call D::consensus_decode because we need to give up ownership, while we we want to use the d again, on another field. Also, note that our escape hatch is calling with &mut d, which is changing the type of the D : Read bound on the trait to &mut D.

The above point might be very important, because even if the compiler can optimize the runtime cost of &mut &mut &mut D it probably can not easily deduplicate monorphizations of all these needless types. So we end up with fn consensus_decode<X>
, fn consensus_decode<&mut X> , fn consensus_decode<&mut &mut X> , fn consensus_decode<&mut &mut X> etc. where X are the real reader types users give us . This is increasing compilation time and/or blowing up the binary size.

As for the looks and amount of noise, apart from death and taxes, the one thing that's certain in this life is appeasing Rust compiler, so we just have to accept it. :D

@Kixunil
Copy link
Collaborator

Kixunil commented Jun 10, 2022

I remember some Rust devs saying d: D is better but the arguments about monomorphisation sound very strong to me and I'm now in favor of d: &mut D. If someone wants to disprove it, bring it on!

@apoelstra
Copy link
Member

I'm also very convinced by the monomorphism argument.

@dpc
Copy link
Contributor Author

dpc commented Jun 10, 2022

Doesn't hurt to get a second opinion: https://users.rust-lang.org/t/taking-r-mut-r-vs-mut-r-r-where-r-io-read/76780

apoelstra added a commit that referenced this issue Jun 29, 2022
1fea098 Support unsized `R` and `W` in consensus encode/decode (Dawid Ciężarkiewicz)
a24a3b0 Forward `consensus_decode` to `consensus_decode_from_finite_reader` (Dawid Ciężarkiewicz)
9c754ca Take Writer/Reader by `&mut` in consensus en/decoding (Dawid Ciężarkiewicz)

Pull request description:

  Fix #1020 (see more relevant discussion there)

  This definitely makes the amount of generics compiler
  has to generate by avoding generating the same functions
  for `R`, `&mut R`, `&mut &mut R` and so on.

  old:

  ```
  > ls -al target/release/deps/bitcoin-07a9dabf1f3e0266
  -rwxrwxr-x 1 dpc dpc 9947832 Jun  2 22:42 target/release/deps/bitcoin-07a9dabf1f3e0266
  > strip target/release/deps/bitcoin-07a9dabf1f3e0266
  > ls -al target/release/deps/bitcoin-07a9dabf1f3e0266
  -rwxrwxr-x 1 dpc dpc 4463024 Jun  2 22:46 target/release/deps/bitcoin-07a9dabf1f3e0266
  ```

  new:

  ```

  > ls -al target/release/deps/bitcoin-07a9dabf1f3e0266
  -rwxrwxr-x 1 dpc dpc 9866800 Jun  2 22:44 target/release/deps/bitcoin-07a9dabf1f3e0266
  > strip target/release/deps/bitcoin-07a9dabf1f3e0266
  > ls -al target/release/deps/bitcoin-07a9dabf1f3e0266
  -rwxrwxr-x 1 dpc dpc 4393392 Jun  2 22:45 target/release/deps/bitcoin-07a9dabf1f3e0266
  ```

  In the unit-test binary itself, it saves ~100KB of data.

  I did not expect much performance gains, but turn out I was wrong(*):

  old:

  ```
  test blockdata::block::benches::bench_block_deserialize                 ... bench:   1,072,710 ns/iter (+/- 21,871)
  test blockdata::block::benches::bench_block_serialize                   ... bench:     191,223 ns/iter (+/- 5,833)
  test blockdata::block::benches::bench_block_serialize_logic             ... bench:      37,543 ns/iter (+/- 732)
  test blockdata::block::benches::bench_stream_reader                     ... bench:   1,872,455 ns/iter (+/- 149,519)
  test blockdata::transaction::benches::bench_transaction_deserialize     ... bench:         136 ns/iter (+/- 3)
  test blockdata::transaction::benches::bench_transaction_serialize       ... bench:          51 ns/iter (+/- 8)
  test blockdata::transaction::benches::bench_transaction_serialize_logic ... bench:           5 ns/iter (+/- 0)
  test blockdata::transaction::benches::bench_transaction_size            ... bench:           3 ns/iter (+/- 0)
  ```

  new:

  ```
  test blockdata::block::benches::bench_block_deserialize                 ... bench:   1,028,574 ns/iter (+/- 10,910)
  test blockdata::block::benches::bench_block_serialize                   ... bench:     162,143 ns/iter (+/- 3,363)
  test blockdata::block::benches::bench_block_serialize_logic             ... bench:      30,725 ns/iter (+/- 695)
  test blockdata::block::benches::bench_stream_reader                     ... bench:   1,437,071 ns/iter (+/- 53,694)
  test blockdata::transaction::benches::bench_transaction_deserialize     ... bench:          92 ns/iter (+/- 2)
  test blockdata::transaction::benches::bench_transaction_serialize       ... bench:          17 ns/iter (+/- 0)
  test blockdata::transaction::benches::bench_transaction_serialize_logic ... bench:           5 ns/iter (+/- 0)
  test blockdata::transaction::benches::bench_transaction_size            ... bench:           4 ns/iter (+/- 0)
  ```

  (*) - I'm benchmarking on a noisy laptop. Take this with a grain of salt. But I think
  at least it doesn't make anything slower.

  While doing all this manual labor that will probably generate conflicts,
  I took a liberty of changing generic type names and variable names to
  `r` and `R` (reader) and `w` and `W` for writer.

ACKs for top commit:
  RCasatta:
    ACK 1fea098 tested in downstream lib, space saving in compiled code confirmed
  apoelstra:
    ACK 1fea098

Tree-SHA512: bc11994791dc97cc468dc9d411b9abf52ad475f23adf5c43d563f323bae0da180c8f57f2f17c1bb7b9bdcf523584b0943763742b81362880206779872ad7489f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants