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

Option::map_or is being instantiated too much #391

Closed
dtolnay opened this issue Dec 5, 2017 · 10 comments · Fixed by #399
Closed

Option::map_or is being instantiated too much #391

dtolnay opened this issue Dec 5, 2017 · 10 comments · Fixed by #399

Comments

@dtolnay
Copy link
Member

dtolnay commented Dec 5, 2017

There should not be 59 copies of this.

$ cargo llvm-lines | head -10

   6085   59  <core::option::Option<T>>::map_or
   1754   74  core::ptr::drop_in_place
   1486   13  <core::result::Result<T, E>>::map
   1188    3  serde_json::read::SliceRead::parse_str_bytes
   1096    1  <&'a mut serde_json::de::Deserializer<R> as serde::de::Deserializer<'de>>::deserialize_any
    931   10  <core::option::Option<T>>::map
    875    8  <core::result::Result<T, E>>::map_err
    862    2  core::str::pattern::TwoWaySearcher::next
    644    2  serde_json::ser::format_escaped_str_contents
    640    2  serde_json::value::ser::<impl serde::ser::Serialize for serde_json::value::Value>::serialize
$ rg '\bmap_or\b'

src/value/de.rs
67:                Ok(Number::from_f64(value).map_or(Value::Null, Value::Number))

src/value/from.rs
68:        Number::from_f64(f).map_or(Value::Null, Value::Number)

src/value/partial_eq.rs
13:        self.as_str().map_or(false, |s| s == other)
19:        self.as_str().map_or(false, |s| s == *other)
25:        other.as_str().map_or(false, |s| s == self)
31:        other.as_str().map_or(false, |s| s == *self)
37:        self.as_str().map_or(false, |s| s == other)
44:        other.as_str().map_or(false, |s| s == self)
53:                    self.$conversion().map_or(false, |i| i == (*other as $base))
59:                    other.$conversion().map_or(false, |i| i == (*self as $base))
65:                    self.$conversion().map_or(false, |i| i == (*other as $base))
71:                    self.$conversion().map_or(false, |i| i == (*other as $base))

src/value/ser.rs
106:        Ok(Number::from_f64(value).map_or(Value::Null, Value::Number))
@arielb1
Copy link

arielb1 commented Dec 15, 2017

I don't think map_or is instantiated too much - it needs to be instantiated at every use-site in order to work, because every use-site passes it a different closure.

However, the initial IR we emit for map_or is garbage quality - see rust-lang/rust#46758 - which causes the IR to bloat.

Note that map_or is this teeny little function, so there should be no reason for it to grow big:

    pub fn map_or<U, F: FnOnce(T) -> U>(self, default: U, f: F) -> U {
        match self {
            Some(t) => f(t),
            None => default,
        }
    }

@cramertj
Copy link

@arielb1

it needs to be instantiated at every use-site in order to work

Right-- but there are way more copies of map_or appearing than there are usages in the code. rust-lang/rust#46477 would account for this, as it's also generating a copy of each map_or for each generic instantiation of the surrounding function. That is, the following would generate six copies, not two:

fn foo<A>(_: A) {
    Some(()).map_or((), |()| println!("foo"));
}
fn foo2<A>(_: A) {
    Some(()).map_or((), |()| println!("foo"));
}

foo(());
foo(5);
foo("");
foo2(());
foo2(5);
foo2("");

@arielb1
Copy link

arielb1 commented Dec 16, 2017

@cramertj

Sure in that case we could potentially merge the specializations, but

  1. map_or is basically a tiny match - there's no good reason it should take an appreciable amount of compilation time (and if it does, then an open-coded match would probably take a similar amount too).
  2. At least in the serde example, map_or is used in PartialEq impls and the closure within it does capture the type parameter, so merging won't help.

BTW:

macro_rules! partialeq_numeric {
($([$($ty:ty)*], $conversion:ident, $base:ty)*) => {
$($(
impl PartialEq<$ty> for Value {
fn eq(&self, other: &$ty) -> bool {
self.$conversion().map_or(false, |i| i == (*other as $base))
}
}
impl PartialEq<Value> for $ty {
fn eq(&self, other: &Value) -> bool {
other.$conversion().map_or(false, |i| i == (*self as $base))
}
}
impl<'a> PartialEq<$ty> for &'a Value {
fn eq(&self, other: &$ty) -> bool {
self.$conversion().map_or(false, |i| i == (*other as $base))
}
}
impl<'a> PartialEq<$ty> for &'a mut Value {
fn eq(&self, other: &$ty) -> bool {
self.$conversion().map_or(false, |i| i == (*other as $base))
}
}
)*)*
}
}
partialeq_numeric! {
[i8 i16 i32 i64 isize], as_i64, i64
[u8 u16 u32 u64 usize], as_u64, u64
[f32 f64], as_f64, f64
[bool], as_bool, bool
}

This is a macro - I see 13*4=52 distinct non-generic calls to map_or in that block, all unmergable because they have different types. Together with the 6 other closure calls, and the 2 identical calls to map_or(Value::Null, Value::Number), all monomorphizations are accounted for.

@arielb1
Copy link

arielb1 commented Dec 16, 2017

So I don't think map_or is instantiated too much, but rather:

  1. There are 58 different PartialEq impls - none of them generic - and all have to be codegen-ed
  2. The codegen for map_or is terrible. I'm trying to figure out how to improve this.

@dtolnay
Copy link
Member Author

dtolnay commented Dec 17, 2017

Sorry the OP is worded in a confusing way. I did not dig into this much before filing the issue but from looking at it now, this is a serde_json bug not a rustc bug. It is computing:

// impl PartialEq<i8> for Value
self.as_i64().map_or(false, |i| i == (*other as i64))

// impl PartialEq<i16> for Value
self.as_i64().map_or(false, |i| i == (*other as i64))

// impl PartialEq<i32> for Value
self.as_i64().map_or(false, |i| i == (*other as i64))

// impl PartialEq<i64> for Value
self.as_i64().map_or(false, |i| i == (*other as i64))

// impl PartialEq<isize> for Value
self.as_i64().map_or(false, |i| i == (*other as i64))

// etc

We (in serde_json) should be able to rearrange this to instantiate map_or much less. Maybe not exactly the following code but conceptually:

let other = *other as i64;
self.as_i64().map_or(false, |i| i == other)

@dtolnay
Copy link
Member Author

dtolnay commented Dec 17, 2017

Until rust-lang/rust#46477 is addressed, we will need a helper function instead of calling map_or directly from each PartialEq impl.

eq_i64(self, *other as i64)

fn eq_i64(value: &Value, other: i64) {
    // instantiate map_or once
    value.as_i64().map_or(false, |i| i == other)
}

@arielb1
Copy link

arielb1 commented Dec 18, 2017

And even with it addressed, each PartialEq impl in a macro will use a different closure, and therefore will be un-mergeable.

@dtolnay
Copy link
Member Author

dtolnay commented Dec 18, 2017

Hmm I don't think so @arielb1. We would use one helper function across Value == u64, u64 == Value, &Value == u64, and &mut Value == u64. We would use another one across Value == str, Value == &str, str == Value, &str == Value, Value == String, and String == Value. In the end I think there will be 6 instantiations of map_or (down from 59).

@arielb1
Copy link

arielb1 commented Dec 18, 2017

@dtolnay

If you move everything to helper functions, then you won't need rust-lang/rust#46477

@dtolnay
Copy link
Member Author

dtolnay commented Dec 18, 2017

Oh I see what you mean. Yes that is correct. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging a pull request may close this issue.

3 participants