Discuss Scratch

Parle--G
Scratcher
3 posts

Help with Arbitrary Precision Addition project...

Hello everyone… I recently made a full precision addition operator project… Here it is.
I optimised it as much as I can, but on checking runtime growth for different digits addition, I noticed quadratic O(n^2) growth.
I believe it is the join string block which does that, maybe copying the whole string, only for adding each part.

I also added a simple “memo” optimization, but quickly understood that it wouldn't help much, and it didn't.

Are the scratch strings immutable? If yes, any method to bypass this bottleneck? Any help would be appreciated….

Also, I stumbled across this project by @odin123com549. It uses digit by digit calculation and directly uses string operations, without using lists at all (I am currently concerned only with addition) but still manages to get equal runtimes, if not faster.

Thanks and Regards,
Parle–G
Jonathan50
Scratcher
1000+ posts

Help with Arbitrary Precision Addition project...

JS string concatenation is supposed to be really efficient. Your code is linear time if you just drop the part to construct the output string, right? Of course you could just use lists for arguments and output (only converting to a string for user display), but in Scratch that comes with its own difficulties…

Last edited by Jonathan50 (Jan. 30, 2026 21:55:30)

Parle--G
Scratcher
3 posts

Help with Arbitrary Precision Addition project...

Thanks for the reply…

While it's true that is string concatenation is efficient, I speculate that scratch blocks have substantial overhead, which is messing with JS' theoretic approach. I understand that using lists for operations subsequent to the assembly might be a good option…
(I say might because coding list operations are another hassle… But maybe it could help with place-value aided operations? )

Also, why is @odin123com549's project calculating at approximately the same speed as mine? He is using base-10, compared to my “base-10^9” , which shall (?) be doing 9x more calculations than mine (or I think so…)

His/her code for reference:

define (a) + (b)
set [a index v] to (length of (a::custom)::operators)
set [b index v] to (length of (b::custom)::operators)
set [carry v] to [0]
set [result v] to []
repeat until <<<(a index) \< [1]> and <(b index) \< [1]>> and <(carry) = [0]>>
set [num v] to (((letter (a index) of (a::custom)) + (letter (b index) of (b::custom))) + (carry))
set [result v] to (join ((num) mod (10)) (result))
set [carry v] to ([floor v] of ((num) / (10))::operators)
change [a index v] by (-1)
change [b index v] by (-1)
end

Total work = n^2/2 (approx, neglect n/2)

Here is my code:

define str.add (num1) (num2)
set [iA v] to (length of (num1::custom)::operators)
set [iB v] to (length of (num2::custom)::operators)
set [str.cbit v] to [0]
delete all of [str.mid v]
set [str.out v] to []
repeat until <<(iA) \< [1]> and <(iB) \< [1]>>
set [str.init v] to ((iA) - (8))
if <(str.init) \< [1]> then
set [str.init v] to [1]
end
set [fA v] to []
set [j v] to (str.init)
repeat (((iA) - (str.init)) + (1))
set [fA v] to (join (fA) (letter (j) of (num1::custom)))
change [j v] by (1)
end
set [iA v] to ((str.init) - (1))
set [str.init v] to ((iB) - (8))
if <(str.init) \< [1]> then
set [str.init v] to [1]
end
set [fB v] to []
set [j v] to (str.init)
repeat (((iB) - (str.init)) + (1))
set [fB v] to (join (fB) (letter (j) of (num2::custom)))
change [j v] by (1)
end
set [iB v] to ((str.init) - (1))
set [str.sum v] to (((fA) + (fB)) + (str.cbit))
set [str.cbit v] to ([floor v] of ((str.sum) / (1000000000))::operators)
set [str.seg v] to ((str.sum) mod (1000000000))
if <<(iA) \> [0]> or <(iB) \> [0]>> then
repeat ((9) - (length of (str.seg)::operators))
set [str.seg v] to (join [0] (str.seg))
end
end
add (str.seg) to [str.mid v]
end
if <(str.cbit) \> [0]> then
add (str.cbit) to [str.mid v]
end
set [i v] to (length of [str.mid v]::data)
set [str.memo v] to []
repeat (i)
set [str.memo v] to (join (str.memo) (item (i) of [str.mid v]))
change [i v] by (-1)
if <((i) mod (50)) = [0]> then
set [str.out v] to (join (str.out) (str.memo))
set [str.memo v] to []
end
end
set [str.out v] to (join (str.out) (str.memo))

Total work = n^2/18 (approx, neglect n/2)

So, is it the overhead which is causing my program to slow down?


Also, on checking the runtime in my project for the first few thousand digits, it scaled somewhat linearly… E.g. 0.524s for two 100k digits, and 1.017s for 200k digits. Why?? (It's a bit strange, or so I think, because at 100k, 200k digits, the “quadratic effect” should not be negligible…)


Is append faster than prepend in scratch too?
(join [...] [new string])//append
(join [new string] [...])//prepend

Which is faster? “join(…)(n characters)” carried out one time, VS “join(…)(one character)” repeated n times
(Or maybe there is an optimal balance between no. of characters and repetitions? Or nested join?)

Is append amortised O(1) and prepend O(n) , or are both O(n)? I guess scratch strings are immutable…
Also, about list operations, isn't “add item to list xyz” block O(1) too?
Why then, they create too much overhead, causing my program to slow down?


Also is there any other optimization that I'm missing?


I think of binary join (after pair-handling) , where
  • I join item 1 & 2, item 3 & 4, …. item n-1 & n resulting in n/2 strings.
  • I join pair 1 & 2, pair 3 & 4, …. pair n/2 -1 & n/2 resulting in n/4 strings.
  • … and so on, till I get one long string?

Sorry for the long blabbery, but I would really appreciate if someone can help me out…

Thanks and Regards,
Parle–G

P.S. - On an unrelated note, does anyone know how to write mathematical notation in the Scratch forums? I need to show some facts and workings related to my project, which requires the ability to display symbols like summations (Σ), matrices, and other technical notation…

Last edited by Parle--G (Yesterday 21:27:04)

Jonathan50
Scratcher
1000+ posts

Help with Arbitrary Precision Addition project...

Both run at least two blocks per digit of each operand, right (since you need to do a substring operation, though that's O(1)), the difference being yours avoids character-by-character joining with long strings?

Parle--G wrote:

While it's true that is string concatenation is efficient, I speculate that scratch blocks have substantial overhead, which is messing with JS' theoretic approach.
Shouldn't really,
    join (args) {
        return Cast.toString(args.STRING1) + Cast.toString(args.STRING2);
    }
but the actual interpretation overhead might do a lot to outweigh the time actually spent by the computation of an individual block. (So running a project in TurboWarp might give quite different results)
Is append amortised O(1) and prepend O(n) , or are both O(n)? I guess scratch strings are immutable…
I only know what I saw from this SO question, I think, via an old Scratch post by comp09…

comp09 wrote:

It depends on the JavaScript engine, but string concatenation in JavaScript is not necessarily not constant time.
where most of the answers are from 2011, and apparently FF uses ropes for strings. My small amount of googling didn't really help me find if that's still true today, but if it is and if this
I think of binary join (after pair-handling) , where
  • I join item 1 & 2, item 3 & 4, …. item n-1 & n resulting in n/2 strings.
  • I join pair 1 & 2, pair 3 & 4, …. pair n/2 -1 & n/2 resulting in n/4 strings.
  • … and so on, till I get one long string?
is a way for the string concatenations to result in a more balanced tree then maybe that'll be fast if you're dedicated?
P.S. - On an unrelated note, does anyone know how to write mathematical notation in the Scratch forums? I need to show some facts and workings related to my project, which requires the ability to display symbols like summations (Σ), matrices, and other technical notation…
Not with BBCode, all I can think of is to write your equation in TeX or MS Office or LibreOffice and upload it as an image.

My earlier point was basically a bignum implementation in just about any other programming language wouldn't do the work of converting back and forth to strings, which is only desirable because of Scratch's very limited typing.

Last edited by Jonathan50 (Today 10:51:23)

Parle--G
Scratcher
3 posts

Help with Arbitrary Precision Addition project...

Then I guess I'll have to make use of list operations…. Thanks for your help!

Even so, could you please explain the linear time behaviour in my project? Was I wrong to suspect O(n^2) ?
This linear time growth continues for as far as I have checked, from 1k to 1m digits…

Eg. At 200k, it was ≈ 1.017 s. At 400k, it was ≈ 2.52 s. From linear scaling, 1m should be ≈7.03s. On actual checking, it was ≈6.607s!
Why is this so? JS strings are immutable… So string assembly should be quadratic time… or I'm missing something?

Also, please suggest better optimizations if you know some (ik I'll use lists, but knowing the best moves is helpful) … I don't think any of the current ones are actually helping…

One last thing(s): Please solve this too… This one would be of great help in optimizations here and henceforth.

Parle--G wrote:

Which is faster? “join(…)(n characters)” carried out one time, VS “join(…)(one character)” repeated n times
(Or maybe there is an optimal balance between no. of characters and repetitions? Or nested join?)

Thanks and Regards,
Parle–G
Jonathan50
Scratcher
1000+ posts

Help with Arbitrary Precision Addition project...

Parle--G wrote:

Which is faster? “join(…)(n characters)” carried out one time, VS “join(…)(one character)” repeated n times
(Or maybe there is an optimal balance between no. of characters and repetitions? Or nested join?)
So, checking SpiderMonkey's source code, Firefox indeed uses ropes among several string representations. https://searchfox.org/firefox-main/source/js/src/vm/StringType.h Comment excerpt:
 * To improve performance of common operations, the following optimizations are
* made which affect the engine's representation of strings:
*
* - The plain vanilla representation is a "linear" string which consists of a
* string header in the GC heap and a malloc'd char array.
*
* - To avoid copying a substring of an existing "base" string , a "dependent"
* string (JSDependentString) can be created which points into the base
* string's char array.
*
* - To avoid O(n^2) char buffer copying, a "rope" node (JSRope) can be created
* to represent a delayed string concatenation. Concatenation (called
* flattening) is performed if and when a linear char array is requested. In
* general, ropes form a binary dag whose internal nodes are JSRope string
* headers with no associated char array and whose leaf nodes are linear
* strings.
*
* - To avoid copying the leftmost string when flattening, we may produce an
* "extensible" string, which tracks not only its actual length but also its
* buffer's overall size. If such an "extensible" string appears as the
* leftmost string in a subsequent flatten, and its buffer has enough unused
* space, we can simply flatten the rest of the ropes into its buffer,
* leaving its text in place. We then transfer ownership of its buffer to the
* flattened rope, and mutate the donor extensible string into a dependent
* string referencing its original buffer.
*
* (The term "extensible" does not imply that we ever 'realloc' the buffer.
* Extensible strings may have dependent strings pointing into them, and the
* JSAPI hands out pointers to linear strings' buffers, so resizing with
* 'realloc' is generally not possible.)
*
* - To avoid allocating small char arrays, short strings can be stored inline
* in the string header (JSInlineString). These come in two flavours:
* JSThinInlineString, which is the same size as JSString; and
* JSFatInlineString, which has a larger header and so can fit more chars.
*
* - To avoid comparing O(n) string equality comparison, strings can be
* canonicalized to "atoms" (JSAtom) such that there is a single atom with a
* given (length,chars).
The actual concatenation code always make a rope unless one of the strings is empty or the result is shorter than 24 bytes of characters. So barring that a string gets flattened somehow(?), on Firefox at least
(join [] [])
really is constant time (the 1st choice is definitely better if the 2nd string already exists). Prepending a char just sticks it on the left, appending sticks it on the right, and however the results are nested shouldn't matter…
Was I wrong to suspect O(n^2) ?
Given the above, yeah, (I don't know what browser you use), only in theory you can't totally depend on the browser's behavior.

And actually trying your project rather than assuming something in it is quadratic time, I was totally wrong to think the last loop contributes that much at all. (For me 1 million digits takes ~3.5 s, and without the last loop it's ~3.5 s again though it was ~3 s the first time I tried that way) For all I know you might already have about as fast a big integer addition block as is possible in Scratch… Lists might help with a tiny bit of a constant factor speedup if there's a good way to do it (IDK if maybe timing the 3 repeats within the main loop separately might be a good idea, to tell you if you could actually save much time by reimplementing). But of course if you have more than 1 number in a list the 200,000 limit will be a bigger problem…

Last edited by Jonathan50 (Today 11:26:16)

Powered by DjangoBB