Skip to content

Fix memory usage for polymorphic types. #7049

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

Merged
merged 7 commits into from
Apr 23, 2025
Merged

Conversation

kwxm
Copy link
Contributor

@kwxm kwxm commented Apr 17, 2025

Fixes https://github.com/IntersectMBO/plutus-private/issues/1498

While costing the array builtins it appeared that the time taken by listToArray differed for different types of list elements. What was actually happening was that the list was wrapped in a ListCostedByLength wrapper but this was ignored when the denotation was executed, so the default size measure was used instead, and this measured the total memory footprint of the list. So two lists of the same length could have different "sizes" and when working out the dependency of time on size you would get different slopes for lists with elements of different types.

This fixes that problem (which should only happen when SomeConstant is involved, which at present only happens for polymorphic types) by changing the default memory usage for lists and arrays to be the length, and the memory usage for pairs to be equal to 1. This is arguably the correct thing to do because polymorphic builtins should be parametrically polymorphic, and the time taken by such functions should not depend on the contents of polymorhpic arguments (although if you really want to you can do weird things with the builtin machinery, so it may be possible to produce counterexamples to this) . The polymorphic builtins which have previously been deployed on the chain all have constant time and memory usage, so this change will not affect any existing code. The only function which this will affect is listToArray, which has not yet been deployed.

This also has the benefit of making a number of Agda conformance tests pass which did not pass previously, because the Agda code has the default cost measures hard-coded and doesn't know about newtype wrappers for alternative size measures. Since we now only have one size measure for the polymorphic types this problem has now gone away for polymorphic functions, although it still persists for a few non-polymorphic functions.

This may still be a bit fragile: see https://github.com/IntersectMBO/plutus-private/issues/1046 for a possible longer-term solution.

@kwxm kwxm added Builtins Costing Anything relating to costs, fees, gas, etc. No Changelog Required Add this to skip the Changelog Check labels Apr 17, 2025
@kwxm kwxm requested review from Unisay, zliu41 and effectfully April 17, 2025 01:21
@kwxm kwxm temporarily deployed to github-pages April 17, 2025 01:21 — with GitHub Actions Inactive
for polymorphic types].
-}

{- Note [ExMemoryUsage for polymorphic types]. For polymorphic types such as
Copy link
Contributor Author

@kwxm kwxm Apr 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@effectfully I'm not confident that this comment is completely accurate, so feel free to make corrections.

Copy link
Contributor Author

@kwxm kwxm Apr 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should say something about this in Builtins.hs as well, although there's already hundreds of lines of comments in there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great comment.

Yeah, maybe Builtins needs to talk about costing too, but we can do that separately.

Comment on lines 194 to 195
memoryUsage _ = singletonRose 1
{-# INLINE memoryUsage #-}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indentation is inconsistent across these instances: some use 4 spaces, some 3.
I suggest raking the whole file with fourmolu

Suggested change
memoryUsage _ = singletonRose 1
{-# INLINE memoryUsage #-}
memoryUsage _ = singletonRose 1
{-# INLINE memoryUsage #-}

Copy link
Contributor Author

@kwxm kwxm Apr 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest raking the whole file with fourmolu

I'll do that before merging. It changes almost every line though, so I won't do it right now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

1 + defaultConstantMeasure (tmCon t x)
+ defaultConstantMeasure (tmCon u y)
defaultConstantMeasure (tmCon (list t) l) = Utils.length l
defaultConstantMeasure (tmCon (pair t u) (x , y)) = 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering why pair has cost 1 a not, say, 2? What logic is applied here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What logic is applied here?

No logic. Nothing we ever do with pairs should depend on the size (famous last words), so it shouldn't matter what it is.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may, if add a builtin serializing a list of pairs or whatever.

But for the time being let's just make it maxBound or whatever instead of 1? So that if our assumption that we'll never need a builtin where that matters gets violated, we'll notice that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may, if add a builtin serializing a list of pairs or whatever.

I don't think that we can do that polymorphically unless we assume that everything in the universe of types is serialisable. We could write a function that would serialise (integer,bytestring) pairs or something but that would be monomorphic and I think that in that case it'd be ok to use a wrapper.

{-# INLINE memoryUsage #-}

instance ExMemoryUsage [a] where
memoryUsage l = singletonRose . fromIntegral $ length l
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
memoryUsage l = singletonRose . fromIntegral $ length l
memoryUsage = singletonRose . fromIntegral . length

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason I thought that we weren't doing that in this file, but maybe I'm just imagining that.

{-# INLINE memoryUsage #-}

instance ExMemoryUsage (Vector a) where
memoryUsage l = singletonRose . fromIntegral $ Vector.length l
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
memoryUsage l = singletonRose . fromIntegral $ Vector.length l
memoryUsage = singletonRose . fromIntegral . Vector.length

Copy link
Contributor

@Unisay Unisay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! (modulo formatting/typos).

@kwxm kwxm temporarily deployed to github-pages April 17, 2025 11:36 — with GitHub Actions Inactive
Copy link
Contributor

@effectfully effectfully left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great stuff.

for polymorphic types].
-}

{- Note [ExMemoryUsage for polymorphic types]. For polymorphic types such as
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great comment.

Yeah, maybe Builtins needs to talk about costing too, but we can do that separately.

{- A naive traversal for size. This accounts for the number of nodes in a Data
object and also the sizes of the contents of the nodes. This is not ideal,
but it seems to be the best we can do. At present this only comes into play
for 'equalsData', which is implemented using the derived implementation of
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not just equalsData, but also serialiseData?

1 + defaultConstantMeasure (tmCon t x)
+ defaultConstantMeasure (tmCon u y)
defaultConstantMeasure (tmCon (list t) l) = Utils.length l
defaultConstantMeasure (tmCon (pair t u) (x , y)) = 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may, if add a builtin serializing a list of pairs or whatever.

But for the time being let's just make it maxBound or whatever instead of 1? So that if our assumption that we'll never need a builtin where that matters gets violated, we'll notice that.

@kwxm kwxm temporarily deployed to github-pages April 17, 2025 22:05 — with GitHub Actions Inactive
@zliu41
Copy link
Member

zliu41 commented Apr 21, 2025

What was actually happening was that the list was wrapped in a ListCostedByLength wrapper but this was ignored when the denotation was executed

Why was it ignored- is it a bug and is it fixed in this PR? I'm asking because there are other newtype wrappers like NumBytesCostedAsNumWords and we don't want those to be ignored.

@kwxm
Copy link
Contributor Author

kwxm commented Apr 22, 2025

Why was it ignored- is it a bug and is it fixed in this PR? I'm asking because there are other newtype wrappers like NumBytesCostedAsNumWords and we don't want those to be ignored.

@effectfully will be able to fill in the details, but my understanding is that this only happens for wrappers around polymorphic types: the code that converts those back into Haskell types is unaware of the wrappers and so it always uses the default size measure. For monomorphic types like integers, bytestrings, etc, the wrappers are recognised and the correct size measure is used. This hasn't shown up before because all of the functions that we have that operate on polymorphic types were constant time, unlike listToArray.

@kwxm kwxm temporarily deployed to github-pages April 23, 2025 18:18 — with GitHub Actions Inactive
@kwxm kwxm enabled auto-merge (squash) April 23, 2025 18:21
@kwxm kwxm merged commit 04c0cf0 into master Apr 23, 2025
7 checks passed
@kwxm kwxm deleted the kwxm/costing/fix-list-memusage branch April 23, 2025 19:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Builtins Costing Anything relating to costs, fees, gas, etc. No Changelog Required Add this to skip the Changelog Check
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants