data-encoding icon indicating copy to clipboard operation
data-encoding copied to clipboard

`Encoding::encode_mut` is very code-size heavy

Open GnomedDev opened this issue 1 year ago • 14 comments

Hello, I've just ran cargo-bloat on my project as I'm working on reducing compile times and binary sizes can be a proxy for compile times, and found that data_encoding::Encoding::encode_mut is taking up 76.9kb , the third largest function in my entire project (that pulls reqwest, tungstenite, etc).

If code size isn't a concern for this project, I'm entirely fine with this being closed, but thought that you might want a heads up that this library is somewhat code-size heavy.

GnomedDev avatar Feb 01 '24 22:02 GnomedDev

Thanks for opening an issue! Yes, binary size is a concern for this library, but only with LTO (because it heavily relies on the actual encoding being known at compile time). Are you measuring bloat with full LTO enabled? If yes, could you provide me reproduction steps? There wasn't any heavy benchmarks in that regard and also no CI to check for regressions, so might be a valid issue :-/

ia0 avatar Feb 01 '24 22:02 ia0

The reproduction I have is that project, but I don't have enough energy to look into this much further. It uses thin lto, but it's probably a bad idea to rely on LTO in the first place. My suggestion would be to feature-lock each encoding to allow dependents to only enable what they need, avoiding the need to rely on LTO.

GnomedDev avatar Feb 01 '24 23:02 GnomedDev

I'm back from vacation and could take a look. However I'm not able to reproduce. I tried cargo bloat --release in your repo but I only see:

0.0%   0.1%  16.1KiB   data_encoding data_encoding::Encoding::encode_mut

Could you provide me with exact instructions to reproduce (including commit hash). I have an idea that could easily fix the issue but I want to confirm it actually fixes your problem first.

Thanks!

ia0 avatar Feb 18 '24 17:02 ia0

image

My project also gets this for the encoding and decoding function

Relevant usage of the library

#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize)]
pub struct PublicKey(pub [u8; 32]);

impl Display for PublicKey {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&HEXLOWER_PERMISSIVE.encode(&self.0))
    }
}

impl FromStr for PublicKey {
    type Err = RouteWeaverError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(PublicKey(
            HEXLOWER_PERMISSIVE
                .decode(s.as_bytes())
                .map_err(|_| RouteWeaverError::KeyFailedToParse)?
                .try_into()
                .map_err(|_| RouteWeaverError::KeyFailedToParse)?,
        ))
    }
}

#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize, ZeroizeOnDrop)]
pub struct PrivateKey(pub [u8; 32]);

impl Display for PrivateKey {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&HEXLOWER_PERMISSIVE.encode(&self.0))
    }
}

impl FromStr for PrivateKey {
    type Err = RouteWeaverError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(PrivateKey(
            HEXLOWER_PERMISSIVE
                .decode(s.as_bytes())
                .map_err(|_| RouteWeaverError::KeyFailedToParse)?
                .try_into()
                .map_err(|_| RouteWeaverError::KeyFailedToParse)?,
        ))
    }
}

It's not the end of the world its just certainly very strange

lambdadeltakay avatar Mar 04 '24 06:03 lambdadeltakay

Thanks! I was able to reproduce on your project with cargo bloat -p route-weaver-router --release. I'll take a look when I get time. I'm quite busy (and a bit sick) at the moment. I'll ping the issue when I have something.

ia0 avatar Mar 04 '24 19:03 ia0

Sorry for the long delay. I tried to #[inline(always)] the API but the dead code is still not eliminated, so I went with the manual solution of providing features. In the route-weaver-router reproduction case, this means changing the dependency from:

[dependencies]
data-encoding = "2.5"

to:

[dependencies.data-encoding]
version = "2.6"  # not released yet
default-features = false
features = ["std", "base16"]

The code size measured with the text column of cargo size -p route-weaver-router --release reduces from 2029548 to 1965156 bytes (64392 less). The data-encoding functions measured with cargo bloat -p route-weaver-router --release --crates -n50 | grep data_encoding reduce from 72.2KiB to 12.6KiB (59.6KiB less).

It is probably possible to do better but it would require quite some refactoring. Would this change be enough for you?

The change is in #101 and I'll keep it open until I decide whether it should be a breaking change or not. It is technically one for those who disable default features.

ia0 avatar Mar 23 '24 13:03 ia0

Actually, I'll probably postpone submitting that PR until I experiment with another idea that would bring much more benefit. I'm just afraid that it might make the API a bit less usable by having many type parameters (a little bit like RustCrypto does). Because if I'll have to make a major version bump, then let's try to make the most out of it.

ia0 avatar Mar 24 '24 20:03 ia0

Thank you for looking into it

lambdadeltakay avatar Mar 25 '24 15:03 lambdadeltakay

Ok, my experiment using StaticEncoding<B: Static<usize>> (and making Encoding a dyn object) seems to provide similar benefits, but doesn't need features. It might still be a breaking change because I'll have to change the constants from const to static. This also would need more work, so not sure how long it will take.

ia0 avatar Mar 29 '24 23:03 ia0

Do you think this is working around a LLVM quirk?

lambdadeltakay avatar Mar 30 '24 04:03 lambdadeltakay

I don't think so. The current design assumes inlining and dead-code elimination to work cross-crate. This seems to work rather well with LTO but not otherwise. The new design I have in mind (I didn't fully test it though) doesn't rely on inlining by adding one layer of indirection (which can be optimized by inlining) and simplifies dead-code elimination by introducing a symbol for each combination, so the linker can always do it regardless of LTO.

ia0 avatar Mar 30 '24 12:03 ia0

Ahh I see. Well thank you for figuring it out

lambdadeltakay avatar Mar 31 '24 00:03 lambdadeltakay

Ok, I got some time to look into this again. I've created the v3-preview branch for now. It will eventually be merged in the main branch. It essentially provides a v3-preview feature that provides a v3_preview module. This module would eventually be the 3.0.0 of data-encoding (but for now is unstable and uses a higher MSRV).

I did the following change:

diff --git a/common/Cargo.toml b/common/Cargo.toml
-data-encoding = "2.5"
+data-encoding = { git = "https://github.com/ia0/data-encoding.git", branch = "v3-preview", features = ["v3-preview"] }
diff --git a/common/src/noise.rs b/common/src/noise.rs
-use data_encoding::HEXLOWER_PERMISSIVE;
+use data_encoding::v3_preview::HEXLOWER_PERMISSIVE;

For the following measurements:

cargo size cargo bloat
before 2093644 72.1KiB
after 2017828 219B
diff 75816 73611

Where "cargo size" is cargo size -p route-weaver-router --release and "cargo bloat" is cargo bloat -p route-weaver-router --release --crates -n100 | grep data_encoding.

So this looks pretty good so far. I'll have to finish adding all features to the v3_preview module and see if I want to add other things. I'll ping here when the v3-preview branch makes it to main, then I'll wait a month or so and do a release, at which point I'll probably close this issue.

ia0 avatar Apr 28 '24 20:04 ia0

Thank you, this is very cool. I am redoing my project and planned to use data-encoding for other things than printing and parsing public/private keys so the added efficiency is amazing.

lambdadeltakay avatar Apr 29 '24 03:04 lambdadeltakay

This is now merged in main. It will eventually be part of 3.0.0 (tracked by #106) but will probably be released as a "preview" module too (to catch possible usability issues).

ia0 avatar May 05 '24 14:05 ia0