Skip to main content

Indie game storeFree gamesFun gamesHorror games
Game developmentAssetsComics
SalesBundles
Jobs
TagsGame Engines
(1 edit)

I couldn’t figure out a good way to benchmark it, but this feels snappier, unless I’m experiencing placebo. Certainly, I wasn’t seeing any speedup in the listener operations (did a simple fibonacci) but I could do something like paste a lot of text into a field and the UI wouldn’t completely bog down. It was still responsive.

Another one is Wigglypaint marker didn’t skip for me. Normally with my laptop (old Thinkpad) Wigglypaint JS, the marker is not a fluid line. I’m pretty sure this isn’t placebo, but again, I couldn’t figure out a good benchmark other than “feeling” the snappiness.

patch to lil.js that turns operation look ups to be inverse hashmaps:

diff --git a/js/lil.js b/js/lil.js
old mode 100755
new mode 100644
index 5c89b4d..07163fa
--- a/js/lil.js
+++ b/js/lil.js
@@ -237,6 +237,7 @@ monad={
 		return r
 	},
 }
+monadk=Object.fromEntries(Object.keys(monad).map((e,i)=>[e,i]))
 dyad={
 	'+':  vd((x,y)=>lmn(ln(x)+ln(y))),
 	'-':  vd((x,y)=>lmn(ln(x)-ln(y))),
@@ -453,6 +454,7 @@ table_swap_rows=(tab,a,b)=>{
 	if(a==b||a<0||b<0||a>tab_rowcount(tab)-1||b>tab_rowcount(tab)-1)return tab
 	const pv=monad.range(lmn(tab_rowcount(tab)));pv.v[a]=lmn(b),pv.v[b]=lmn(a);return dyad.take(pv,tab)
 }
+dyadk=Object.fromEntries(Object.keys(dyad).map((e,i)=>[e,i]))
 triad={
 	'@orderby': (col,tab,order_dir)=>{
 		const rt=orderby(tab,dyad.take(lmn(count(tab)),lml(ll(col))).v,ln(order_dir))
@@ -477,8 +479,9 @@ triad={
 		return lin(x)?r:dyad[','](lt(x),r)
 	},
 }
+triadk=Object.fromEntries(Object.keys(triad).map((e,i)=>[e,i]))
 
-findop=(n,prims)=>Object.keys(prims).indexOf(n), as_enum=x=>x.split(',').reduce((x,y,i)=>{x[y]=i;return x},{})
+findop=(n,prims)=>prims[n], as_enum=x=>x.split(',').reduce((x,y,i)=>{x[y]=i;return x},{})
 let tnames=0;tempname=_=>lms(`@t${tnames++}`)
 op=as_enum('JUMP,JUMPF,JUMPT,LIT,DUP,DROP,SWAP,OVER,BUND,OP1,OP2,OP3,GET,SET,LOC,LOCS,AMEND,TAIL,CALL,BIND,ITER,EACH,NEXT,COL,IPRE,IPOST,FIDX,FMAP')
 oplens=   [ 3   ,3    ,3    ,3  ,1  ,1   ,1   ,1   ,3   ,3  ,3  ,3  ,3  ,3  ,3  ,3   ,3    ,1   ,1   ,1   ,1   ,3   ,3   ,1  ,3   ,3    ,3   ,3    ]
@@ -492,9 +495,9 @@ blk_gets=(x,i  )=>0xFFFF&(blk_getb(x,i)<<8|blk_getb(x,i+1))
 blk_op  =(x,o  )=>{blk_addb(x,o);if(o==op.COL)blk_addb(x,op.SWAP)}
 blk_opa =(x,o,i)=>{blk_addb(x,o),blk_adds(x,i);return blk_here(x)-2}
 blk_imm =(x,o,k)=>{let i=x.locals.findIndex(x=>match(x,k));if(i==-1)i=x.locals.length,x.locals.push(k);blk_opa(x,o,i)}
-blk_op1 =(x,n)=>blk_opa(x,op.OP1,findop(n,monad))
-blk_op2 =(x,n)=>blk_opa(x,op.OP2,findop(n,dyad ))
-blk_op3 =(x,n)=>blk_opa(x,op.OP3,findop(n,triad))
+blk_op1 =(x,n)=>blk_opa(x,op.OP1,findop(n,monadk))
+blk_op2 =(x,n)=>blk_opa(x,op.OP2,findop(n,dyadk ))
+blk_op3 =(x,n)=>blk_opa(x,op.OP3,findop(n,triadk))
 blk_lit =(x,v)=>blk_imm(x,op.LIT,v)
 blk_set =(x,n)=>blk_imm(x,op.SET,n)
 blk_loc =(x,n)=>blk_imm(x,op.LOC,n)
@@ -659,10 +662,10 @@ parse=text=>{
 			blk_op3(b,'@ins');return
 		}
 		if(matchsp('(')){if(matchsp(')')){blk_lit(b,lml([]));return}expr(b),expect(')');return}
-		const s=peek().v;if(findop(s,monad)>=0&&({'symbol':1,'name':1})[peek().t]){
+		const s=peek().v;if(findop(s,monadk)!==undefined&&({'symbol':1,'name':1})[peek().t]){
 			next();if(matchsp('@')){
 				let depth=0,l=lmblk();while(matchsp('@'))depth++
-				expr(b),blk_opa(l,op.FMAP,findop(s,monad))
+				expr(b),blk_opa(l,op.FMAP,findop(s,monadk))
 				while(depth-->0){const t=tempname(),m=lmblk();blk_loop(m,[ls(t)],_=>{blk_get(m,t),blk_cat(m,l)}),l=m}
 				blk_cat(b,l)
 			}else{expr(b),blk_op1(b,s)};return
@@ -678,7 +681,7 @@ parse=text=>{
 			blk_sets(l,fidx,blk_here(l))
 			while(depth-->0){const t=tempname(),m=lmblk();blk_loop(m,[ls(t)],_=>{blk_get(m,t),blk_cat(m,l)}),l=m}
 			blk_cat(b,l);return
-		}const s=peek().v;if(findop(s,dyad)>=0&&({'symbol':1,'name':1})[peek().t]){next(),expr(b),blk_op2(b,s)}
+		}const s=peek().v;if(findop(s,dyadk)!==undefined&&({'symbol':1,'name':1})[peek().t]){next(),expr(b),blk_op2(b,s)}
 	}
 	const b=lmblk();if(hasnext())expr(b);while(hasnext())blk_op(b,op.DROP),expr(b)
 	if(blk_here(b)==0)blk_lit(b,NIL);return b
(3 edits)

Oh - the hashes probably aren’t right if you try to do a git apply. It’s from my fork of decker that I use for WebXDC. And, I fixed a small thing just now in the patch that does !==undefined where it was doing >=0 before (from indexOf to an Object/hashmap).

And, yeah, I loaded a Wigglypaint in the old version, and I can tell the difference between the old and new. It’s a lot smoother.

(1 edit)

I’m less sure if this helps, because on top of the other one, I can’t tell if it does. I’ll have to test without the other optimization.

I know if I do this sort of lookup dispatch in my C forths/lisps, they don’t help. Usually the switch-case is best - probably due to inlining rather than function pointer tables. I’ve never sat down to look, just empirical testing. But, maybe it helps in JS.

turns the switch-case into a lookup table:

diff --git a/js/lil.js b/js/lil.js
index 31b10f4..a88963c 100644
--- a/js/lil.js
+++ b/js/lil.js
@@ -714,50 +714,52 @@ docall=(f,a,tail)=>{
 	issue(f.a.length==1&&f.a[0][0]=='.'?env_bind(f.c,[f.a[0].slice(3)],monad.list(a)): env_bind(f.c,f.a,a),f.b)
 	calldepth=max(calldepth,state.e.length)
 }
+ops={
+	[op.DROP ]:(o,imm,b)=>{arg()},
+	[op.DUP  ]:(o,imm,b)=>{const a=arg();ret(a),ret(a);},
+	[op.SWAP ]:(o,imm,b)=>{const a=arg();b=arg();ret(a),ret(b);},
+	[op.OVER ]:(o,imm,b)=>{const a=arg();b=arg();ret(b),ret(a),ret(b);},
+	[op.JUMP ]:(o,imm,b)=>{setpc(imm)},
+	[op.JUMPF]:(o,imm,b)=>{if(!lb(arg()))setpc(imm)},
+	[op.JUMPT]:(o,imm,b)=>{if( lb(arg()))setpc(imm)},
+	[op.LIT  ]:(o,imm,b)=>{ret(blk_getimm(b,imm))},
+	[op.GET  ]:(o,imm,b)=>{ret(env_get(getev(),blk_getimm(b,imm)));},
+	[op.SET  ]:(o,imm,b)=>{const v=arg();env_set(getev(),blk_getimm(b,imm),v),ret(v);},
+	[op.LOC  ]:(o,imm,b)=>{const v=arg();env_local(getev(),blk_getimm(b,imm),v),ret(v);},
+	[op.LOCS ]:(o,imm,b)=>{env_locals(getev(),blk_getimm(b,imm));},
+	[op.BUND ]:(o,imm,b)=>{const r=[];for(let z=0;z<imm;z++)r.push(arg());r.reverse(),ret(lml(r));},
+	[op.OP1  ]:(o,imm,b)=>{                      ret(monadi[imm](arg()));},
+	[op.OP2  ]:(o,imm,b)=>{const         y=arg();ret(dyadi [imm](arg(),y));},
+	[op.OP3  ]:(o,imm,b)=>{const z=arg(),y=arg();ret(triadi[imm](arg(),y,z));},
+	[op.IPRE ]:(o,imm,b)=>{const s=arg(),i=arg();ret(i),docall(s,i.v[imm]);if(lion(s)||lii(s)||linat(s)){for(let z=0;z<=imm;z++)i.v[z]=null}},
+	[op.IPOST]:(o,imm,b)=>{const s=arg(),i=arg(),r=arg();ret(i.v[imm]?r:s),ret(i),ret(s);},
+	[op.AMEND]:(o,imm,b)=>{
+			let v=arg(),r=arg(),i=ll(arg()),ro=arg(),n=blk_getimm(b,imm),t={v:1}
+			if(i.length&&!i[0]){i=i.filter(x=>x),t.v=0}r=amendv(ro,i,v,0,t);if(t.v&&!lin(n))env_set(getev(),n,r);ret(r);
+		},
+	[op.CALL ]:(o,imm,b)=>{const a=arg(),f=arg();docall(f,a,o==op.TAIL);}, // same as tail
+	[op.TAIL ]:(o,imm,b)=>{const a=arg(),f=arg();docall(f,a,o==op.TAIL);},
+	[op.BIND ]:(o,imm,b)=>{const f=arg(),r=lmon(f.n,f.a,f.b);r.c=getev(),env_local(getev(),lms(f.n),r),ret(r);},
+	[op.ITER ]:(o,imm,b)=>{const x=arg();ret(lil(x)?x:ld(x));ret(lid(x)?lmd():lml([]));},
+	[op.FIDX ]:(o,imm,b)=>{const x=arg(),f=arg();if((lid(f)||lil(f)||lis(f))&&lil(x)){ret(lml(x.v.map(x=>l_at(f,x))));setpc(imm)}else{ret(x)};},
+	[op.FMAP ]:(o,imm,b)=>{const x=arg(),f=monadi[imm];ret(lid(x)?lmd(x.k,x.v.map(f)):lml(ll(x).map(f)));},
+	[op.EACH ]:(o,imm,b)=>{
+			const n=arg(),r=arg(),s=arg();if(count(r)==count(s)){setpc(imm),ret(r);}
+			else{const z=count(r), v=lml([s.v[z],lid(s)?s.k[z]:lmn(z),lmn(z)]);
+			state.e.push(env_bind(getev(),n,v)),ret(s),ret(r);}
+		},
+	[op.NEXT ]:(o,imm,b)=>{const v=arg(),r=arg(),s=arg();state.e.pop();if(lid(r))r.k.push(s.k[r.v.length]);r.v.push(v),ret(s),ret(r),setpc(imm);},
+	[op.COL  ]:(o,imm,b)=>{
+			const ex=arg(),t=arg(),n=tab_cols(t),v=ll(monad.cols(t));ret(t)
+			n.push('column'),v.push(t),issue(env_bind(getev(),n,lml(v)),ex);
+  },
+}
 runop=_=>{
 	op_count+=1
 	const b=getblock();if(!liblk(b))ret(state.t.pop())
 	const pc=getpc(),o=blk_getb(b,pc),imm=(oplens[o]==3?blk_gets(b,1+pc):0); setpc(pc+oplens[o])
-	switch(o){
-		case op.DROP :arg();break
-		case op.DUP  :{const a=arg();ret(a),ret(a);break}
-		case op.SWAP :{const a=arg(),b=arg();ret(a),ret(b);break}
-		case op.OVER :{const a=arg(),b=arg();ret(b),ret(a),ret(b);break}
-		case op.JUMP :setpc(imm);break
-		case op.JUMPF:if(!lb(arg()))setpc(imm);break
-		case op.JUMPT:if( lb(arg()))setpc(imm);break
-		case op.LIT  :ret(blk_getimm(b,imm));break
-		case op.GET  :{ret(env_get(getev(),blk_getimm(b,imm)));break}
-		case op.SET  :{const v=arg();env_set(getev(),blk_getimm(b,imm),v),ret(v);break}
-		case op.LOC  :{const v=arg();env_local(getev(),blk_getimm(b,imm),v),ret(v);break}
-		case op.LOCS :{env_locals(getev(),blk_getimm(b,imm));break}
-		case op.BUND :{const r=[];for(let z=0;z<imm;z++)r.push(arg());r.reverse(),ret(lml(r));break}
-		case op.OP1  :{                      ret(monadi[imm](arg()    ));break}
-		case op.OP2  :{const         y=arg();ret(dyadi [imm](arg(),y  ));break}
-		case op.OP3  :{const z=arg(),y=arg();ret(triadi[imm](arg(),y,z));break}
-		case op.IPRE :{const s=arg(),i=arg();ret(i),docall(s,i.v[imm]);if(lion(s)||lii(s)||linat(s)){for(let z=0;z<=imm;z++)i.v[z]=null}break}
-		case op.IPOST:{const s=arg(),i=arg(),r=arg();ret(i.v[imm]?r:s),ret(i),ret(s);break}
-		case op.AMEND:{
-			let v=arg(),r=arg(),i=ll(arg()),ro=arg(),n=blk_getimm(b,imm),t={v:1}
-			if(i.length&&!i[0]){i=i.filter(x=>x),t.v=0}r=amendv(ro,i,v,0,t);if(t.v&&!lin(n))env_set(getev(),n,r);ret(r);break
-		}
-		case op.CALL : // fall through:
-		case op.TAIL :{const a=arg(),f=arg();docall(f,a,o==op.TAIL);break}
-		case op.BIND :{const f=arg(),r=lmon(f.n,f.a,f.b);r.c=getev(),env_local(getev(),lms(f.n),r),ret(r);break}
-		case op.ITER :{const x=arg();ret(lil(x)?x:ld(x));ret(lid(x)?lmd():lml([]));break}
-		case op.FIDX :{const x=arg(),f=arg();if((lid(f)||lil(f)||lis(f))&&lil(x)){ret(lml(x.v.map(x=>l_at(f,x))));setpc(imm)}else{ret(x)};break}
-		case op.FMAP :{const x=arg(),f=monadi[imm];ret(lid(x)?lmd(x.k,x.v.map(f)):lml(ll(x).map(f)));break}
-		case op.EACH :{
-			const n=arg(),r=arg(),s=arg();if(count(r)==count(s)){setpc(imm),ret(r);break}
-			const z=count(r), v=lml([s.v[z],lid(s)?s.k[z]:lmn(z),lmn(z)]);
-			state.e.push(env_bind(getev(),n,v)),ret(s),ret(r);break
-		}
-		case op.NEXT :{const v=arg(),r=arg(),s=arg();state.e.pop();if(lid(r))r.k.push(s.k[r.v.length]);r.v.push(v),ret(s),ret(r),setpc(imm);break}
-		case op.COL  :{
-			const ex=arg(),t=arg(),n=tab_cols(t),v=ll(monad.cols(t));ret(t)
-			n.push('column'),v.push(t),issue(env_bind(getev(),n,lml(v)),ex);break
-		}
-	}while(running()&&getpc()>=blk_here(getblock()))descope()
+	ops[o](o,imm,b)
+	while(running()&&getpc()>=blk_here(getblock()))descope()
 }
 
 fchar=x=>x=='I'?'i': x=='B'?'b': x=='L'?'s': x=='t'?'J': x=='T'?'J': x
Deleted 19 days ago

Both in conjunction appear to help. Wigglypaint is the smoothest with both patches. I can draw with the pen and the marker doesn’t skip when it paints under it. Normally, it skips a lot for me. Also, the other way I can tell is if I load my Huffman deck and paste a large amount of text it it. Usually the menu and the listener are nearly impossible to use. With these, it’s somewhat acceptable, though still slow.

I didn’t know what was the best way other than eyeballing it. I can tell it’s smoother, but for “raw computation” it doesn’t make a difference. I don’t know if that’s due to how things get timesliced between rendering and compute.

If you can tell me a proper way to benchmark this, I can do the hard work to test it if it’s worthwhile - I know empirical eyeballing is a bit hard to judge.

https://codeberg.org/woodring/decker.decker-xdc/src/branch/decker-xdc/noxdc.html contains both fixes for comparison