Bytecode size bloat with array function parameter

Hi guys, I am searching for a reason why our bytecode is so immensely big (>3.5 million bytes) and I stumbled upon this really weird pattern, so I would like to know whether this is expected behavior.

As of now, I can only find this behavior in combination with using U256, although this is specific to our contracts and I wouldn’t rule out the effect of other var types. Given this array param behavior, it is not surprising that our bytcodesize is that bloated as we are using similar functions a lot.
To recreate what I’m seeing, you can use the following sample contract and uncomment the function calls in each setup independently (the setups work mutually exclusive!):

contract;

use std::u256::U256;

abi MyContract {
   fn foo();
}

impl MyContract for Contract {
    fn foo() {
      // Declaring a bunch of test vars
        let var0: u64 = 1;
        let var1: u64 = 2;
        let var2: u64 = 3;
        // ***** Byte code only var declaration: 68 bytes *****

        // Calling the test functions
        // Setup 1): One call without array param
        // let res0 = multiply_div(var2, var1, var0);
        // ***** Byte code setup 1): 14,100 bytes *****

        // Setup 2): Two calls without array param
        // let res0 = multiply_div(var2, var1, var0);
        // let res1 = multiply_div(var2, var1, var0);
        // ***** Byte code setup 2): 14,364 bytes *****

        // Setup 3): One call WITH array param
        // let res0 = multiply_div_array(var2, var1, var0, [1, 1]);
        // ***** Byte code setup 3): 14,140 bytes *****

        // Setup 4): Two calls WITH array param
        // let res0 = multiply_div_array(var2, var1, var0, [1, 1]);
        // let res1 = multiply_div_array(var2, var1, var0, [1, 1]);
        // ***** Byte code setup 4): 29,940 bytes *****
    }
}

fn multiply_div(a: u64, b: u64, c: u64) -> u64 {

    let var = (U256::from((0, 0, 0, a)) * U256::from((0, 0, 0, b)));
    let result_wrapped = (var / U256::from((0, 0, 0, c))).as_u64();

    match result_wrapped {
        Result::Ok(inner_value) => inner_value, _ => revert(0), 
    }
}

fn multiply_div_array(a: u64, b: u64, c: u64, some_array: [u64; 2]) -> u64 {

    let var = (U256::from((0, 0, 0, a)) * U256::from((0, 0, 0, b)));
    let result_wrapped = (var / U256::from((0, 0, 0, c))).as_u64();

    match result_wrapped {
        Result::Ok(inner_value) => inner_value, _ => revert(0), 
    }
}
1 Like

So to give some insight, this is expected behavior as far as I can tell, because multiplying two U256s requires an obscene amount of instructions on a 64-bit word size, because its 4 limbs * 4 limbs (which is 4^4 instructions not counting any carries, etc), and then there are plenty of intermediate values and carries in between there. I might recommend checking out bn_mult if you are multiplying a U256 by a non U256 in order to cut down on instructions used, and coming changes to the std-lib and language should also help reduce binary size

1 Like

Gotcha, thank you for the info.
I continued to experiment with our contracts and just removing the array param does not fix copying the bytecode as the code above would indicate. So there seems to be more to it.
I’m still confused why the code above shows that just multiplying U256 variables doesn’t hurt the bytecode size too much. At least with the sample above, it is only when you include the array param when bytecode of the function is being copied. I get your answer with the instructions but I’m having a hard time imagining that the array param (which is not even being used btw) has anything to do with that.

I guess to put it simply, my question is:
Is it normal that including an array parameter in a function call leads to copying the entire function bytecode every time I call the function (as shown in the example above)?

Is this normal?
Can somebody reproduce?
And what else leads to this copy behavior?