Skip to content

Conversation

@Unisay
Copy link
Contributor

@Unisay Unisay commented Sep 18, 2025

Summary

This PR implements cost modeling for Value-related builtins: lookupCoin, valueContains, valueData, and unValueData.

Implementation

Complete cost modeling pipeline:

  • Cost model infrastructure and parameter definitions
  • Benchmarking framework with realistic Cardano constraints
  • Statistical analysis with R models (linear/constant based on performance characteristics)
  • Updated JSON cost model configurations across all versions

Cost models:

  • valueData: Uses constant cost model based on uniform performance analysis
  • lookupCoin: Linear cost model with dimension reduction for 3+ parameters
  • valueContains: Linear cost model for container/contained size dependency
  • unValueData: Linear cost model for size-dependent deserialization

All functions now have proper cost models instead of unimplemented placeholders.

@Unisay Unisay self-assigned this Sep 18, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Sep 18, 2025

PR Preview Action v1.6.2

🚀 View preview at
https://IntersectMBO.github.io/plutus/pr-preview/pr-7344/

Built to branch gh-pages at 2025-09-19 08:01 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@Unisay Unisay force-pushed the yura/costing-builtin-value branch 6 times, most recently from 528ebcd to 69f1d6f Compare September 24, 2025 16:06
@Unisay Unisay changed the title WIP: Add costing for lookupCoin and valueContains builtins Cost models for LookupCoin, ValueContains, ValueData, UnValueData builtins Sep 24, 2025
@Unisay Unisay marked this pull request as ready for review September 24, 2025 16:24
@Unisay Unisay requested review from ana-pantilie and kwxm September 24, 2025 16:41
@Unisay Unisay force-pushed the yura/costing-builtin-value branch 3 times, most recently from 53d9ea1 to 5b60cfc Compare September 30, 2025 10:15
@Unisay Unisay force-pushed the yura/costing-builtin-value branch from 5b60cfc to 7eebe28 Compare October 2, 2025 09:43
Copy link
Contributor

@kwxm kwxm left a comment

Choose a reason for hiding this comment

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

Here are some initial comments. I'll come back and add some more later. I need to look at the benchmarks properly though.

@Unisay Unisay force-pushed the yura/costing-builtin-value branch from b1a6bf1 to 6afef50 Compare October 9, 2025 14:11
@Unisay Unisay requested a review from zliu41 October 9, 2025 14:20

-- | Generate random key as ByteString (for lookup arguments)
generateKeyBS :: (StatefulGen g m) => g -> m ByteString
generateKeyBS = uniformByteStringM Value.maxKeyLen
Copy link
Member

Choose a reason for hiding this comment

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

If the keys are completely random, then lookupCoin will probably never hit an existing entry, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

lookupCoin will probably never hit an existing entry,

Maybe that's what we want? Do we know if finding out that something's not in the map is the worst case? Naively you might think that the time taken to discover that some key is not in the map is always greater or equal to the time taken to find a key that is in the map.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't agree. I think we should actively include both the case when the map contains the key and when it doesn't. Otherwise we're not really measuring this case, and that's the whole point of benchmarking, right? Otherwise we would just use the, analytically discovered, worst-time complexity of the algorithm and pick a function from that category for its cost, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, as I mentioned above, you won't have a good idea of the actual size of the Value if you don't enforce uniqueness of the keys.

@Unisay Unisay force-pushed the yura/costing-builtin-value branch from 3cee663 to 86d645a Compare October 10, 2025 10:26
@Unisay
Copy link
Contributor Author

Unisay commented Oct 10, 2025

I have simplified the generators (less fixed values, more randomly generated samples, quantities are all maxBound :: Int64)

After that I've re-benchmarked and re-generated cost models. This is how I view them:

LookupCoin

LookupCoin

ValueContains

ValueContains

ValueData

ValueData

UnValueData

UnValueData

CC: @kwxm

Copy link
Member

@zliu41 zliu41 left a comment

Choose a reason for hiding this comment

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

In order to benchmark the worst case, I think you should also ensure that lookupCoin always hits the largest inner map (or at least, such cases should be well-represented).

Also, we'll need to re-run benchmarking for unValueData after adding the enforcement of integer range.

@@ -12094,203 +12094,710 @@ IndexArray/42/1,1.075506579052359e-6,1.0748433439930302e-6,1.0762684407023462e-6
IndexArray/46/1,1.0697135554442532e-6,1.0690902192698813e-6,1.0704133377013816e-6,2.2124820728450233e-9,1.8581237858977844e-9,2.6526943923047553e-9
IndexArray/98/1,1.0700747499373992e-6,1.0693842628239684e-6,1.070727062396803e-6,2.2506114869928674e-9,1.9376849028666025e-9,2.7564941558204088e-9
IndexArray/82/1,1.0755056682976695e-6,1.0750405368241111e-6,1.076102212770973e-6,1.8355219893844098e-9,1.5161640335164335e-9,2.4443625958006994e-9
Bls12_381_G1_multiScalarMul/1/1,8.232134704712041e-5,8.228195390475752e-5,8.23582682466318e-5,1.224261187989977e-7,9.011720721178711e-8,1.843107342917502e-7
Copy link
Contributor

@kwxm kwxm Oct 10, 2025

Choose a reason for hiding this comment

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

GitHub seeems to think that the data for all of the BLS functions has changed, but I don't think they have.

Copy link
Contributor Author

@Unisay Unisay Oct 13, 2025

Choose a reason for hiding this comment

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

The file on master contains Windows-style line terminators (\r\n) for BLS lines:

git show master:plutus-core/cost-model/data/benching-conway.csv | grep "Bls12_381_G1_multiScalarMul/1/1" | od -c | grep -C1 "\r"
0000000   B   l   s   1   2   _   3   8   1   _   G   1   _   m   u   l
0000020   t   i   S   c   a   l   a   r   M   u   l   /   1   /   1   ,
0000040   8   .   2   3   2   1   3   4   7   0   4   7   1   2   0   4
--
0000200   8   7   1   1   e   -   8   ,   1   .   8   4   3   1   0   7
0000220   3   4   2   9   1   7   5   0   2   e   -   7  \r  \n

This PR changes \r\n to \n .

let prefixLen = Value.maxKeyLen - 4
prefix = BS.replicate prefixLen (0xFF :: Word8)
-- Encode the integer in big-endian format (last 4 bytes)
b0 = fromIntegral $ (n `shiftR` 24) .&. 0xFF
Copy link
Member

Choose a reason for hiding this comment

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

you can simply generate 4 random bytes, instead of generating an integer and doing these bitwise operations.

[] -- no type arguments needed (monomorphic builtin)
(lookupCoinArgs gen) -- the argument combos to generate benchmarks for

lookupCoinArgs :: StdGen -> [(ByteString, ByteString, Value)]
Copy link
Contributor

@kwxm kwxm Nov 9, 2025

Choose a reason for hiding this comment

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

Maps are implemented as balanced binary trees with (key, value) pairs at the internal nodes, so if you've got a node containing key k then the left subtree of the node will only contain keys less than k and the right subtree keys greater than k. The functions that operate on maps are supposed to keepthe tree balanced, so the left subtree should be (approximately) the same size as the right one. When you've got 2^n-1 nodes the tree should be prefectly balanced and every path from the root to a leaf should have length n (ie, you should pass through n nodes as you travel from the root to an entry both of whose subtrees are empty (Tip)).

I think that to get the worst case behaviour for lookupCoin you can generate an outer map with 2^a - 1 unique keys for a in some range like 1..10 or 1..15, so you get a full tree. Looking for the entry with the largest key should then require searching all the way to the bottom of the tree (always branching to the right), which should be the worst case (and also make sure that all of the keys have a long common prefix to maximise the comparison time). The inner map for this longest case should also be a full binary tree, with 2^b - 1 entries for some b; we want the worst case to search the inner map as well, and in this case that should happen when you look for a key that's bigger than all of the keys in the inner map, since again you'll have to search all the way down the right hand side of the tree; I think that if you search for the biggest key it'll take pretty much the same time though. The total time taken should be proportional to a+b, since you'll have so examine a nodes in the outer map and b in the inner map, and since the keys are of the same type for inner and outer maps the time taken per node should be about the same for both (if the key types in the inner and outer maps were different then you might be looking at something of the form ra+sb, but here r and s should be the same, so you've got r(a+b)).

Note that a and b are the depths of the trees, which wil be integerLog2 of the number of entries, so I think here you want to use a size measure which is integerLog2(outer size) + integerLog2(maximum inner size).

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess you'll need to benchmark this over some set of pairs of depths (a,b) where a and b both vary over [1..15] or something, but that might take a long time since for that range you'd be running 225 benchmarks. I think that in fact the time will only depend on a+b, but initially we should check that that's true by looking at different values of a and b with the same sum.

Copy link
Member

Choose a reason for hiding this comment

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

It might be an overkill to bother generating full trees. In any balanced binary tree the depths of any two leaves differ by at most 1, so as long as we make sure that we hit a leaf node (using either the smallest key or the largest key), 🤷

It's more important to make sure the outer key hits the largest inner map. I think the worst case is: there's a large inner map with N/2 keys, together with N/2 singleton inner maps, and the outer key hits that large inner map. Whether the outer map and the inner map are full trees shouldn't matter that much.

Copy link
Member

Choose a reason for hiding this comment

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

So in summary, I would do this:

  • Given total size N, let there be roughly N/2 inner maps: one big inner map whose size is roughly N/2, and the rest are singletons.
  • Outer key should hit the big inner map.
  • Both the outer key and the inner key should hit leaf nodes. So both keys should be either the min or the max key in the respective map (or the inner key may be absent in the inner map).

This should be very close to the worst case, if not the worst case. I wouldn't bother with varying a and b, or generating full trees, or anything like that.

Copy link
Contributor

@kwxm kwxm Nov 10, 2025

Choose a reason for hiding this comment

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

I wouldn't bother with varying a and b

I think it's worth doing that at least once just to make sure that the time taken depends only on the sum of a and b . If we can show that it does then we can just restrict the benchmarking to the case when a = 1, so you only have one entry in the outer map; alternatively, you can take b = 1 and just worry about the size of the outer map. I think we can effectively regard the entire map as one big tree: when you get to the tip of the outer map you move into one of the inner maps and continue searching there, so it's like the inner maps are glued onto the leaves of the outer map. Then all that matters is the total depth, a+b. I'd like to check that assumption before going any further though: it's better to have some evidence than just to guess what's going on.

Copy link
Member

Choose a reason for hiding this comment

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

If we can show that it does then we can just restrict the benchmarking to the case when a = 1, so you only have one entry in the outer map; alternatively, you can take b = 1 and just worry about the size of the outer map.

You can't. lookupCoin is O(log max(m, k)). So it's better to balance the size of the outer map and the size of the largest inner map.

Copy link
Member

Choose a reason for hiding this comment

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

In other words, given a size measure of 10, you can:

  • Make the outer map have 1024 entries, and the largest inner map have 1024 entries
  • Or make the outer map have 1024 entries, and the largest inner map have 1 entry
  • Or make the outer map have 1 entry, and the largest inner map have 1024 entries

Obviously the first is the worst case

Copy link
Contributor

@kwxm kwxm Nov 10, 2025

Choose a reason for hiding this comment

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

You can't. lookupCoin is O(log max(m, k))

No, I don't think it is. I think it's O(log a + log b), or at least that log a + log b is a size measure that will give a more precise result than log(max(a, b) , and that's what my proposed experiment is trying to confirm. The O's are obscuring what's actually going on since they're hiding the details of the constants. For the sum the actual costing function will be of the form r + s(log a+ log b) and for the maximum it'll be of the form u + v*log(max(a, b)). Now log a+log b <= 2*max(log a, log b), with equality when a=b, so r+s*(log a + log b) <= r + 2*s*max(log a, log b), and when you put an O round them they become the same, but the right hand one can actually be almost twice the left hand one.

In other words, given a size measure of 10, you can:

Make the outer map have 1024 entries, and the largest inner map have 1024 entries
Or make the outer map have 1024 entries, and the largest inner map have 1 entry
Or make the outer map have 1 entry, and the largest inner map have 1024 entries

Obviously the first is the worst case

With the sum size measure the first map has a size of 20 and the other ones have a size of 11, but with the maximum they all have size 10, so the sum version picks out the worst case and the maximum doesn't. A stragiht line fitted to the benchmarking results using the sum measure should give us a more precise bound for the execution time than if we use the maximum measure. The sum is a more accurate measure of the total depth of a value than the maximum, and I think that the total depth is what we need to worry about.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I made benchmarking and here is what it shows (LLM summary):

Two-Level Map Lookup Performance: Experimental Analysis

  Experimental Setup

  Data structure: Map ByteString (Map ByteString Natural)

  Key characteristics:
  - 32-byte ByteStrings with 28-byte common prefix (simulating hash-like keys)
  - Perfect binary tree structure (keys = 2^n - 1)
  - Worst-case lookup: deepest key in both outer and inner maps

  Test parameters:
  - Outer map depth: a (containing 2^a - 1 keys)
  - Inner map depth: b (containing 2^b - 1 keys)
  - Total depth: N = a + b

  ---
  Experiment 1: Distribution Impact (Constant Total Depth)

  Hypothesis: When N is held constant, how does the distribution of depth between outer and inner maps
  affect performance?

  Test configuration: N = 17, varying distributions from (1,16) to (16,1)

  Results: Distribution Has Minimal Impact

  | Distribution   | Outer Keys | Inner Keys | Lookup Time | Deviation from Mean |
  |----------------|------------|------------|-------------|---------------------|
  | (10,7)         | 1,023      | 127        | 1.657 μs    | +1.5%               |
  | (6,11)         | 63         | 2,047      | 1.655 μs    | +1.4%               |
  | (8,9) balanced | 255        | 511        | 1.644 μs    | +0.7%               |
  | (1,16) extreme | 1          | 65,535     | 1.607 μs    | -1.6%               |
  | (16,1) extreme | 65,535     | 1          | 1.598 μs    | -2.1%               |

  Range: 1.598 μs to 1.657 μs (3.6% variation)

  Key finding: Distribution choice has negligible impact on performance when total depth is constant.

  ---
  Experiment 2: Linear Scaling with Total Depth

  Hypothesis: Lookup time scales linearly with total depth N = a + b, regardless of distribution.

  Test configuration: N ∈ {10, 12, 14, 16, 18, 20}, three representative distributions per N

  Results: Strong Linear Relationship

  | N (Total Depth) | Representative Samples  | Min Time | Max Time | Average Time | Cost per Level |
  |-----------------|-------------------------|----------|----------|--------------|----------------|
  | 10              | (1,9), (5,5), (9,1)     | 125.6 ns | 128.3 ns | 127.0 ns     | —              |
  | 12              | (1,11), (6,6), (11,1)   | 132.9 ns | 142.9 ns | 136.9 ns     | +4.95 ns       |
  | 14              | (1,13), (7,7), (13,1)   | 140.1 ns | 154.0 ns | 144.5 ns     | +3.80 ns       |
  | 16              | (1,15), (8,8), (15,1)   | 145.3 ns | 160.4 ns | 151.1 ns     | +3.30 ns       |
  | 18              | (1,17), (9,9), (17,1)   | 152.7 ns | 167.6 ns | 158.6 ns     | +3.75 ns       |
  | 20              | (1,19), (10,10), (19,1) | 158.8 ns | 173.9 ns | 168.8 ns     | +5.10 ns       |

  Average cost per level: 4.18 ns

  Linear model: Time ≈ 80 ns + 4.2 ns × N (R² ≈ 0.99)

  ---
  Scaling Analysis

  | Test                   | Expected (if linear) | Observed    | Result     |
  |------------------------|----------------------|-------------|------------|
  | N=10 → N=20 (2x depth) | 2.00x time           | 1.33x time  | Sub-linear |
  | Per-level increment    | Constant             | 4.18 ns avg | ✅ Constant |

  Note: The sub-linear scaling (1.33x instead of 2x) suggests the baseline overhead is significant
  relative to per-level cost at small N values.

  ---
  Key Experimental Tendencies

  1. Distribution Independence (Constant N)
    - Variation between distributions: <4%
    - Expensive key comparisons dominate performance
    - Tree shape/cache effects are negligible
  2. Linear Depth Scaling
    - Each additional tree level: +4.2 ns
    - Baseline overhead: ~80 ns
    - Strong linear correlation (R² ≈ 0.99)
  3. Distribution Variation by Depth

  | Total Depth (N) | Max-Min Spread | % Variation |
  |-----------------|----------------|-------------|
  | 10              | 2.7 ns         | 2.1%        |
  | 12              | 10.0 ns        | 7.3%        |
  | 14              | 13.9 ns        | 9.6%        |
  | 16              | 15.1 ns        | 10.0%       |
  | 18              | 14.9 ns        | 9.4%        |
  | 20              | 15.1 ns        | 8.9%        |

  Pattern: Variation increases slightly at deeper depths but remains <10%

  ---
  Conclusions

  1. For expensive key comparisons (ByteStrings with common prefixes):
    - Lookup time is primarily determined by total depth (a + b)
    - Distribution choice (split between outer/inner) has minimal impact
  2. Performance model:
  Lookup Time ≈ 80 ns + 4.2 ns × (a + b)
  3. Practical implication:
    - When designing nested map structures with expensive keys, optimize for total depth minimization
  rather than specific distribution patterns
    - Tree balancing and cache optimization are secondary concerns
  4. Cost breakdown:
    - ~80 ns: Fixed overhead (function calls, setup)
    - ~4.2 ns per level: Key comparison + tree traversal

Copy link
Member

Choose a reason for hiding this comment

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

I'm fine with using log m + log k instead of log (max m k). I don't think it really matters either way, but let's just make a decision and move forward with getting the costing done.

We need to stay on schedule for HF by EOY, and it is a firm deadline. If both @kwxm and @Unisay prefers log m + log k then go ahead with it.

Unisay and others added 25 commits November 10, 2025 10:04
Extends the cost modeling framework to support lookupCoin, valueContains,
valueData, and unValueData builtins. Adds parameter definitions, arity
specifications, and integrates with the cost model generation system.

Establishes foundation for accurate costing of Value operations in
Plutus Core execution.
Creates Values.hs benchmark module with systematic test generation
for lookupCoin, valueContains, valueData, and unValueData operations.
Includes value generation utilities, individual benchmark functions,
and edge case testing with empty values.

Enables data collection for accurate cost model parameter fitting.
Implements optimal statistical models for Value operations based on
performance characteristics: linear models for lookupCoin and valueContains
(size-dependent), constant model for valueData (uniform performance),
and linear model for unValueData.

Provides accurate cost parameters across all builtin cost model
configurations and updates test expectations.
Removes unimplementedCostingFun placeholders for Value builtins and
connects them to their respective cost model parameters (paramLookupCoin,
paramValueContains, paramValueData, paramUnValueData).

Enables accurate execution cost calculation for Value operations in
Plutus Core scripts.
Includes extensive benchmark results covering various input sizes and
edge cases for lookupCoin, valueContains, valueData, and unValueData.
Data validates the chosen statistical models and cost parameters.

Provides empirical foundation confirming model accuracy across
different operation profiles.
Add a new Logarithmic newtype wrapper in ExMemoryUsage that transforms
size measures logarithmically. This enables linear cost models to
effectively capture O(log n) runtime behavior by measuring log(size)
instead of size directly.

The wrapper computes max(1, floor(log2(size) + 1)) from any wrapped
ExMemoryUsage instance, making it composable with existing size measures
like ValueOuterOrMaxInner for operations with logarithmic complexity.

This infrastructure supports proper costing of Value builtins like
lookupCoin which has O(log max(m, k)) complexity.
Refactor the Value benchmarking suite to use Cardano-compliant key sizes
(32-byte max) and leverage the new Logarithmic wrapper for accurate
modeling of logarithmic operations.

Key changes:
- Apply Logarithmic wrapper to lookupCoin and valueContains benchmarks
  for proper O(log n) cost modeling
- Consolidate key generators from 4 functions to 2, eliminating duplication
- Remove obsolete key size parameters throughout (keys always maxKeyLen)
- Extract withSearchKeys pattern to eliminate repetitive code
- Simplify test generation by removing arbitrary key size variations
- Clean up lookupCoinArgs structure for better readability

The refactoring reduces the module from 359 to 298 lines while improving
clarity and ensuring all generated Values comply with Cardano's 32-byte
key length limit.
Simplify the R model definitions for Value-related builtins by replacing
custom linear model implementation with standard linearInY wrapper for
valueContains. This maintains the same statistical behavior while
improving code maintainability.

Add inline comments documenting the parameter wrapping strategy used
for each model (Logarithmic wrapping for lookupCoin/valueContains,
ValueTotalSize for contains operand, unwrapped for valueData/unValueData).

Clean up formatting inconsistencies in model definitions.
Refreshed benchmarking data for lookupCoin, valueContains, valueData,
and unValueData with improved statistical coverage and sampling.

This data serves as the foundation for the refined cost model
parameters applied in the subsequent commit.
Updated cost parameters based on fresh benchmark data analysis:

- lookupCoin: Adjusted intercept (284421→179661) and slope (1→7151)
  to better reflect actual performance with varying currency counts
- valueContains: Changed from added_sizes to linear_in_y model with
  refined parameters (intercept 42125119→1000, slope 30→130383)
- valueData: Reduced constant cost (205465→153844) based on updated
  profiling results
- unValueData: Switched to linear_in_x model with refined parameters
  (intercept 10532326261→1000, slope 431→33094)

All three cost model variants (A, B, C) updated for consistency.
Modernize logarithm calculation in the Logarithmic ExMemoryUsage instance
by switching from the compatibility module GHC.Integer.Logarithms to the
modern GHC.Num.Integer API.

Changes:
- Replace integerLog2# (unboxed, from GHC.Integer.Logarithms) with
  integerLog2 (boxed, from GHC.Num.Integer)
- Simplify code by removing unboxing boilerplate: I# (integerLog2# x)
  becomes integerLog2 x
- Keep other imports (GHC.Integer.Logarithms, GHC.Exts) as they are still
  used elsewhere in the file (memoryUsageInteger function)

This addresses code review feedback to use the modern ghc-bignum API
instead of the legacy compatibility module, while maintaining the same
computational semantics. Cost model regeneration verified no regression
in derived parameters.
Address Kenneth's review comment by ensuring builtins use the same
size measure wrappers as their budgeting benchmarks.

Changes:
- Add LogValueOuterOrMaxInner newtype combining logarithmic
  transformation with outer/max inner size measurement
- Update lookupCoin and valueContains to use size measure wrappers
- Add KnownTypeAst instances for ValueTotalSize and LogValueOuterOrMaxInner
- Update benchmarks to use new combined wrapper type

This ensures the cost model accurately reflects runtime behavior by
using identical size measures in both denotations and benchmarks.
Regenerate cost model parameters based on fresh benchmark runs for the
four Value-related built-in functions: lookupCoin, valueContains,
valueData, and unValueData.

New cost models:
- lookupCoin: linear_in_z (intercept: 209937, slope: 7181)
- valueContains: linear_in_y (intercept: 1000, slope: 131959)
- valueData: constant_cost (182815)
- unValueData: linear_in_x (intercept: 1000, slope: 33361)

The benchmark data includes 350 measurement points across varying input
sizes to ensure accurate cost estimation. All three cost model variants
(A, B, C) have been updated consistently with identical parameters.
Document the regeneration of benchmark data and cost model parameters
for the four Value-related built-in functions following fresh benchmark
measurements.
…verhead

Regenerate cost model parameters based on fresh benchmark runs after
rebasing on master. This accounts for the negative amount validation
added to valueContains in commit 531f1b8.

Updated cost models:
- lookupCoin: linear_in_z (intercept: 203599, slope: 7256)
- valueContains: linear_in_y (intercept: 1000, slope: 130720)
- valueData: constant_cost (156990)
- unValueData: linear_in_x (intercept: 1000, slope: 36194)

The benchmark data includes 350 measurement points across varying input
sizes. All three cost model variants (A, B, C) have been updated
consistently with identical parameters.
Replace local benchmark data with results from GitHub Actions remote
execution and regenerate cost model parameters for the four Value-related
builtins: lookupCoin, valueContains, valueData, and unValueData.

Remote benchmarking provides more consistent and reliable measurements
by running on standardized infrastructure, eliminating local environment
variations that could affect cost model accuracy.

Updated parameters across all cost model versions (A, B, C):
- lookupCoin: intercept 203599→210606, slope 7256→8019
- valueContains: slope 130720→94161
- valueData: constant 156990→162241
- unValueData: slope 36194→15417
Reformat builtin cost model JSON files to use consistent 4-space
indentation instead of 2-space indentation. This improves readability
and aligns with common JSON formatting conventions for configuration
files.

No semantic changes - only whitespace formatting updated.

Files affected:
- builtinCostModelA.json
- builtinCostModelB.json
- builtinCostModelC.json
Co-authored-by: Kenneth MacKenzie <kenneth.mackenzie@iohk.io>
Apply the same optimization used in the Logarithmic instance to
memoryUsageInteger, using integerLog2 directly instead of unboxed
integerLog2# and quotInt# operations.

This allows us to remove:
- MagicHash language extension
- GHC.Exts imports (Int (I#), quotInt#)
- GHC.Integer and GHC.Integer.Logarithms imports

The refactoring maintains identical functionality while making the code
more consistent and simpler.
Simplifies the memory usage measurement by consolidating three separate types
(Logarithmic, ValueOuterOrMaxInner, LogValueOuterOrMaxInner) into a single
ValueLogOuterOrMaxInner type. This reduces complexity while maintaining the
same functionality for measuring logarithmic Value sizes.

The new type directly encodes the intended semantics: size = log(max(outer, maxInner)),
making the code more maintainable and producing clearer type signatures in builtin
function definitions.
Replaces unsafe fromJust usage with explicit error messages and HasCallStack
constraint in costModelParamsForTesting. This provides better debugging context
when cost model parameter extraction fails, including stack traces that pinpoint
the exact call site.
Adds cost model parameter names for LookupCoin, ValueContains, ValueData,
and UnValueData builtins (11 new parameters per ledger API version). Updates
parameter count expectations to reflect the expanded parameter set.

Updates golden type signatures and conformance test budget expectations to
reflect the refined ValueLogOuterOrMaxInner type signature, ensuring accurate
cost accounting for Value-based operations.
Update CPU cost parameters for lookupCoin, valueContains, valueData, and unValueData based on refined benchmark data using worst-case key generation. The new benchmarks include detailed measurements with granular Value structure information (outer/inner map sizes).

This update reflects more accurate costs after implementing worst-case ByteString key generation that forces full-length comparisons during Map lookups, providing conservative estimates for on-chain scenarios.
@Unisay Unisay force-pushed the yura/costing-builtin-value branch from 5b8d02e to bfd577b Compare November 11, 2025 11:16
@Unisay Unisay requested a review from ana-pantilie November 11, 2025 14:13
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 this pull request may close these issues.

5 participants