diff --git a/src/classfile.ts b/src/classfile.ts index 8d0f9ce..a71a991 100644 --- a/src/classfile.ts +++ b/src/classfile.ts @@ -37,24 +37,37 @@ export interface ExceptionEntry { export class Code { readonly maxStack: number; readonly maxLocals: number; - readonly code: Uint8Array; readonly handlers: ExceptionEntry[]; - // raw LocalVariableTable attribute body, parsed on demand - readonly localVars: Uint8Array | null; // the class's bytes, and where its LineNumberTable bodies start (-1 for none) private bytes: Uint8Array; + private codeAt: number; + private codeEnd: number; + private view: Uint8Array | null = null; + // where the LocalVariableTable body starts and ends (-1 for none) + private varsAt: number; + private varsEnd: number; private linesAt: number; private moreLines: number[] | null; private parsedLines: number[] | null | undefined; - constructor(maxStack: number, maxLocals: number, code: Uint8Array, handlers: ExceptionEntry[], bytes: Uint8Array, linesAt: number, moreLines: number[] | null, localVars: Uint8Array | null) { + constructor(maxStack: number, maxLocals: number, codeAt: number, codeEnd: number, handlers: ExceptionEntry[], bytes: Uint8Array, linesAt: number, moreLines: number[] | null, varsAt: number, varsEnd: number) { this.maxStack = maxStack; this.maxLocals = maxLocals; - this.code = code; + this.codeAt = codeAt; + this.codeEnd = codeEnd; this.handlers = handlers; this.bytes = bytes; this.linesAt = linesAt; this.moreLines = moreLines; - this.localVars = localVars; + this.varsAt = varsAt; + this.varsEnd = varsEnd; + } + // the bytecode, as a view made on first use: most methods never run + get code(): Uint8Array { + return this.view ??= this.bytes.subarray(this.codeAt, this.codeEnd); + } + // the raw LocalVariableTable attribute body + get localVars(): Uint8Array | null { + return this.varsAt < 0 ? null : this.bytes.subarray(this.varsAt, this.varsEnd); } // pairs of (start pc, line), sorted by pc get lines(): number[] | null { @@ -86,7 +99,8 @@ export interface Member { access: number; name: string; desc: string; - attrs: Map; + // the attributes besides Code: a map, or where they start in the class file for memberAttrs + attrs: Map | number; code: Code | null; } @@ -191,7 +205,8 @@ function readMember(r: Reader, cp: ClassFile["cp"]): Member { const access = r.u2(); const name = utf8(cp, r.u2()); const desc = utf8(cp, r.u2()); - let attrs = NO_ATTRS; + const at = r.pos; + let attrs: Map | number = NO_ATTRS; let code: Code | null = null; for (let n = r.u2(); n > 0; n--) { const attr = utf8(cp, r.u2()); @@ -201,13 +216,26 @@ function readMember(r: Reader, cp: ClassFile["cp"]): Member { code = readCode(r, cp); r.pos = end; } else { - if (attrs === NO_ATTRS) attrs = new Map(); - attrs.set(attr, r.slice(length)); + attrs = at; + r.pos += length; } } return { access, name, desc, attrs, code }; } +// The attributes of the member whose attribute count is at `at`, but Code. +export function memberAttrs(cp: ClassFile["cp"], at: number): Map { + const r = new Reader(cp.bytes); + r.pos = at; + const attrs = new Map(); + for (let n = r.u2(); n > 0; n--) { + const name = utf8(cp, r.u2()); + const body = r.slice(r.u4()); + if (name !== "Code") attrs.set(name, body); + } + return attrs; +} + function readAttrs(r: Reader, cp: ClassFile["cp"]) { const attrs = new Map(); for (let n = r.u2(); n > 0; n--) { @@ -220,7 +248,9 @@ function readAttrs(r: Reader, cp: ClassFile["cp"]) { function readCode(r: Reader, cp: ClassFile["cp"]): Code { const maxStack = r.u2(); const maxLocals = r.u2(); - const code = r.slice(r.u4()); + const codeAt = r.pos + 4; + const codeEnd = codeAt + r.u4(); + r.pos = codeEnd; let n = r.u2(); const handlers: ExceptionEntry[] = n === 0 ? NO_HANDLERS : []; for (; n > 0; n--) { @@ -228,7 +258,7 @@ function readCode(r: Reader, cp: ClassFile["cp"]): Code { } let linesAt = -1; let moreLines: number[] | null = null; - let localVars: Uint8Array | null = null; + let varsAt = -1, varsEnd = -1; for (n = r.u2(); n > 0; n--) { const name = utf8(cp, r.u2()); const length = r.u4(); @@ -237,11 +267,14 @@ function readCode(r: Reader, cp: ClassFile["cp"]): Code { else (moreLines ??= []).push(r.pos); r.pos += length; } else { - const body = r.slice(length); - if (name === "LocalVariableTable") localVars = body; + if (name === "LocalVariableTable") { + varsAt = r.pos; + varsEnd = r.pos + length; + } + r.pos += length; } } - return new Code(maxStack, maxLocals, code, handlers, r.bytes, linesAt, moreLines, localVars); + return new Code(maxStack, maxLocals, codeAt, codeEnd, handlers, r.bytes, linesAt, moreLines, varsAt, varsEnd); } export function cpEntry(cp: ClassFile["cp"], i: number): CpEntry | undefined { diff --git a/src/jit.ts b/src/jit.ts index b361277..492b6c3 100644 --- a/src/jit.ts +++ b/src/jit.ts @@ -244,25 +244,71 @@ const runtime = { return f; }, // A call that is not direct: the result of a native or an interpreted - // method, PUSHED once the callee's frames must finish in the run loop, or - // INTERPRET for the run loop to make the call. - slowCall(t: JThread, m: JMethod, args: any[]): any { - if (m.isAbstract) return INTERPRET; - if (m.native === null) return m.code ? hooks.callNested(t, m, args) : INTERPRET; + // method. When the callee's frames must finish in the run loop (xp) or the + // run loop must make the call (xi), returns E with the exit code in xc. + slowCall(t: JThread, m: JMethod, args: any[], xp: number, xi: number): any { let r; - try { - r = m.native(t, args); - } catch (e) { - throw fromNative(e, m); + if (m.isAbstract) r = INTERPRET; + else if (m.native === null) r = m.code ? hooks.callNested(t, m, args) : INTERPRET; + else { + try { + r = m.native(t, args); + } catch (e) { + throw fromNative(e, m); + } + if (runtime.isGen(r)) { + t.frames.push(new NativeFrame(r, m, m.ret, false)); + r = PUSHED; + } else if (m.ret === "Z" && typeof r === "boolean") r = r ? 1 : 0; } - if (runtime.isGen(r)) { - t.frames.push(new NativeFrame(r, m, m.ret, false)); - return PUSHED; + if (r === PUSHED) runtime.xc = xp; + else if (r === INTERPRET) runtime.xc = xi; + else return r; + return runtime.E; + }, + E: Object.freeze({ exit: true }), + xc: 0, + // slowCall's counterpart for a virtual call that missed its inlining guard: + // dispatches through the site's cache and calls a compiled target directly. + // Up to four argument slots come as parameters, more in an array. + callv(t: JThread, f: Frame, ic: any[], pc: number, xp: number, xi: number, a0: any, a1?: any, a2?: any, a3?: any): any { + f.pc = pc; + const tm: JMethod = a0 !== null && a0.cls === ic[0] ? ic[1] : runtime.select(a0, ic, pc); + let cf: Frame | null; + if (tm.jit !== null && (cf = t.enter(tm, a0)) !== null) { + if ((tm.jit as (...a: any[]) => number)(t, cf, a0, a1, a2, a3) !== 0) { + t.unpool(cf); + runtime.xc = xp; + return runtime.E; + } + return t.ret; } - return m.ret === "Z" && typeof r === "boolean" ? (r ? 1 : 0) : r; + return runtime.callvSlow(t, tm, [a0, a1, a2, a3], xp, xi); + }, + callvMany(t: JThread, f: Frame, ic: any[], args: any[], pc: number, xp: number, xi: number): any { + f.pc = pc; + const recv = args[0] as JObject | null; + const tm: JMethod = recv !== null && recv.cls === ic[0] ? ic[1] : runtime.select(recv, ic, pc); + let cf: Frame | null; + if (tm.jit !== null && (cf = t.enter(tm, recv)) !== null) { + if ((tm.jit as (...a: any[]) => number)(t, cf, ...args) !== 0) { + t.unpool(cf); + runtime.xc = xp; + return runtime.E; + } + return t.ret; + } + return runtime.callvSlow(t, tm, args, xp, xi); + }, + // a native or interpreted target: slots are argument slots, natives take values + callvSlow(t: JThread, tm: JMethod, slots: any[], xp: number, xi: number): any { + const native: any[] = [slots[0]]; + for (let i = 1, p = 0; p < tm.params.length; p++) { + native.push(slots[i]); + i += isWide(tm.params[p]) ? 2 : 1; + } + return runtime.slowCall(t, tm, native, xp, xi); }, - PUSHED, - INTERPRET, longToFloat, npe: npeAt, arr(o: JArray | null, i: number, pc?: number): any { @@ -474,6 +520,8 @@ function regionInfo(m: JMethod): RegionInfo | null { } catch (e) { if (!(e instanceof Unsupported)) throw e; } + // a method that cannot be a region is analyzed again if it is ever compiled + if (r === null && m !== generating) analyses.delete(m); } regionInfos.set(m, r); return r; @@ -539,6 +587,8 @@ interface Body { region: Region | null; // instructions before this pc were emitted already, as part of a fused sequence skip: number; + // operand stack slots known to hold a non-null value: `this` loaded since the last jump target + nn: Set; } interface Region { @@ -575,32 +625,55 @@ interface Gen { resume: Set; // leave through the shared exit, which spills the frame's state; kind is an EXIT_* value exit: (pc: number, sp: number, kind: number) => string; + // the code of such an exit, for a helper that picks it at run time + xcode: (pc: number, sp: number, kind: number) => number; // run this one instruction in the interpreter slow: (pc: number, sp: number) => string; call: (pc: number, sp: number) => string; } +// the method being compiled +let generating: JMethod | null = null; + function generate(t: JThread, m: JMethod): { src: string; consts: any[] } { + generating = m; + try { + return generateCode(t, m); + } finally { + generating = null; + // inlinable methods keep it in their region info + analyses.delete(m); + } +} + +function generateCode(t: JThread, m: JMethod): { src: string; consts: any[] } { const code = m.code!; const a = analyze(m); const consts: any[] = []; + const index = new Map(); + // a constant used again shares its slot; inline caches pass fresh arrays, so they stay apart const K = (v: any) => { - consts.push(v); - return `K[${consts.length - 1}]`; + let i = index.get(v); + if (i === undefined) { + i = consts.push(v) - 1; + index.set(v, i); + } + return `K[${i}]`; }; const nl = code.maxLocals, ns = code.maxStack + 2; const L = (i: number) => `l${i}`; const S = (i: number) => `s${i}`; const alloc = `if(L===null){L=f.locals=new Array(${nl});S=f.stack=new Array(${ns});}`; const spillLocals = () => alloc + Array.from({ length: nl }, (_, i) => `L[${i}]=${L(i)};`).join(""); - const top: Body = { m, a, L, S, out: [], region: null, skip: 0 }; + const top: Body = { m, a, L, S, out: [], region: null, skip: 0, nn: new Set() }; const resume = new Set(); - const exit = (pc: number, sp: number, kind: number) => { + const xcode = (pc: number, sp: number, kind: number) => { resume.add(pc); - return `{x=${exitCode(pc, sp, kind)};break X;}`; + return exitCode(pc, sp, kind); }; + const exit = (pc: number, sp: number, kind: number) => `{x=${xcode(pc, sp, kind)};break X;}`; const g: Gen = { - t, K, vars: new Set(), regions: [], inlined: 0, top, resume, exit, + t, K, vars: new Set(), regions: [], inlined: 0, top, resume, exit, xcode, slow: (pc, sp) => exit(pc, sp, EXIT_FALLBACK), call: (pc, sp) => exit(pc, sp, EXIT_INTERPRET), }; @@ -635,7 +708,7 @@ function generate(t: JThread, m: JMethod): { src: string; consts: any[] } { const fname = `${m.cls.name}_${m.name}`.replace(/[^A-Za-z0-9_$]/g, "_"); const out: string[] = []; // direct calls pass arguments as parameters and a frame without arrays - out.push(`return (function ${fname}(t,f${params.map((p) => "," + p).join("")}){let L=f.locals,S=f.stack;const vm=t.vm,H=rt.hooks;`); + out.push(`return (function ${fname}(t,f${params.map((p) => "," + p).join("")}){let L=f.locals,S=f.stack;const vm=t.vm,H=rt.hooks,np=rt.npe;`); out.push(`let pc=f.pc,tm,cf,r,a,v,ic,ifs=0,dpc=0,dsp=0,x=0,xs=0,xe=null,${[...g.vars].join(",")};`); out.push(`if(L===null){${params.map((p, i) => `${L(i)}=${p};`).join("")}}else{${Array.from({ length: nl }, (_, i) => `${L(i)}=L[${i}];`).join("")}${Array.from({ length: ns }, (_, i) => `${S(i)}=S[${i}];`).join("")}}`); out.push(`X:{for(;;){try{switch(pc){`); @@ -658,7 +731,7 @@ function deoptCatch(g: Gen): string { const chain: Body[] = []; for (let q: Body = r; q.region; q = q.region.parent) chain.unshift(q); const outer = chain[0].region!; - let s = `case ${r.region!.id}:rt.deopted(${g.K(r.m)},f.m);${stats ? `rt.noteDeopt(${g.K(r.m)},e);` : ""}`; + let s = `case ${r.region!.id}:`; chain.forEach((q, k) => { const code = q.m.code!; const inner = chain[k + 1]?.region; @@ -669,8 +742,9 @@ function deoptCatch(g: Gen): string { }); return s + `x=${exitCode(outer.callPc, outer.base, 0)};break;`; }); + const callee = `${g.K(g.regions.map((r) => r.m))}[ifs-1]`; return `if(ifs!==0){if(e!==rt.DEOPT){dpc=e instanceof rt.PendingThrow&&e.pc!==undefined?e.pc:0;dsp=0;}` + - `switch(ifs){${cases.join("")}}ifs=0;if(f.direct)f.direct=false;` + + `rt.deopted(${callee},f.m);${stats ? `rt.noteDeopt(${callee},e);` : ""}switch(ifs){${cases.join("")}}ifs=0;if(f.direct)f.direct=false;` + `x+=e===rt.DEOPT?${EXIT_SUSPEND}:e instanceof rt.JavaThrow?${EXIT_RETHROW}:${EXIT_ERROR};xe=e;break X;}`; } @@ -688,7 +762,7 @@ function genRegion(g: Gen, parent: Body, callee: JMethod, args: string[], result const single = info.starts.size === 1; const b: Body = { m: callee, a: info.a, L: (i) => `${p}l${i}`, S: (i) => `${p}s${i}`, out: [], - region: { id, level, pcVar: `${p}pc`, result, parent, callPc, base }, skip: 0, + region: { id, level, pcVar: `${p}pc`, result, parent, callPc, base }, skip: 0, nn: new Set(), }; const mark = g.regions.length, inlined = g.inlined; g.regions.push(b); @@ -719,8 +793,40 @@ function genRegion(g: Gen, parent: Body, callee: JMethod, args: string[], result } } -// Emits the instruction at pc of body b. +// Whether `this` of m is never overwritten, so aload_0 is never null. +const keepsThis = new WeakMap(); +function thisKept(m: JMethod): boolean { + let k = keepsThis.get(m); + if (k === undefined) { + const c = m.code!.code; + k = !m.isStatic; + for (const pc of analyze(m).pcs) { + const op = c[pc]; + if (op === 0x4b || (op === 0x3a && c[pc + 1] === 0) || (op === 0xc4 && c[pc + 1] === 0x3a && u16(c, pc + 2) === 0)) k = false; + } + keepsThis.set(m, k); + } + return k; +} + +// Emits the instruction at pc of body b, then tracks which stack slots are +// known to be non-null. function insn(g: Gen, b: Body, pc: number) { + const c = b.m.code!.code, op = c[pc]; + const d = b.a.depth.get(pc)!; + if (b.a.targets.has(pc)) b.nn.clear(); + insnCode(g, b, pc); + if (op === 0x2a || (op === 0x19 && c[pc + 1] === 0)) { + b.nn.delete(d + 1); + if (thisKept(b.m)) b.nn.add(d); + else b.nn.delete(d); + } else if (op >= 0x01 && op <= 0x2d) { + b.nn.delete(d); + b.nn.delete(d + 1); + } else b.nn.clear(); +} + +function insnCode(g: Gen, b: Body, pc: number) { const { m, L, S, region } = b; const t = g.t, vm = t.vm, K = g.K; const cls = m.cls; @@ -772,7 +878,7 @@ function insn(g: Gen, b: Body, pc: number) { case 0x26: case 0x27: case 0x28: case 0x29: emit(`${S(d)}=${L(op - 0x26)};`); break; case 0x2a: case 0x2b: case 0x2c: case 0x2d: emit(`${S(d)}=${L(op - 0x2a)};`); break; case 0x2e: case 0x2f: case 0x30: case 0x31: case 0x32: case 0x33: case 0x34: case 0x35: - emit(`if(${top(1)}===null)rt.npe(${pc});a=${top(1)}.a;if(${top()}>>>0>=a.length)rt.arr(${top(1)},${top()},${pc});${top(1)}=a[${top()}];`); + emit(`if(${top(1)}===null)np(${pc});a=${top(1)}.a;if(${top()}>>>0>=a.length)rt.arr(${top(1)},${top()},${pc});${top(1)}=a[${top()}];`); break; case 0x36: case 0x38: case 0x3a: emit(`${L(c[pc + 1])}=${top()};`); break; case 0x37: case 0x39: emit(`${L(c[pc + 1])}=${top(1)};`); break; @@ -782,13 +888,13 @@ function insn(g: Gen, b: Body, pc: number) { case 0x47: case 0x48: case 0x49: case 0x4a: emit(`${L(op - 0x47)}=${top(1)};`); break; case 0x4b: case 0x4c: case 0x4d: case 0x4e: emit(`${L(op - 0x4b)}=${top()};`); break; case 0x4f: case 0x51: case 0x55: case 0x56: - emit(`if(${top(2)}===null)rt.npe(${pc});a=${top(2)}.a;if(${top(1)}>>>0>=a.length)rt.arr(${top(2)},${top(1)},${pc});a[${top(1)}]=${top()};`); + emit(`if(${top(2)}===null)np(${pc});a=${top(2)}.a;if(${top(1)}>>>0>=a.length)rt.arr(${top(2)},${top(1)},${pc});a[${top(1)}]=${top()};`); break; case 0x54: - emit(`if(${top(2)}===null)rt.npe(${pc});a=${top(2)}.a;if(${top(1)}>>>0>=a.length)rt.arr(${top(2)},${top(1)},${pc});a[${top(1)}]=${top(2)}.cls.name==="[Z"?${top()}&1:${top()};`); + emit(`if(${top(2)}===null)np(${pc});a=${top(2)}.a;if(${top(1)}>>>0>=a.length)rt.arr(${top(2)},${top(1)},${pc});a[${top(1)}]=${top(2)}.cls.name==="[Z"?${top()}&1:${top()};`); break; case 0x50: case 0x52: - emit(`if(${top(3)}===null)rt.npe(${pc});a=${top(3)}.a;if(${top(2)}>>>0>=a.length)rt.arr(${top(3)},${top(2)},${pc});a[${top(2)}]=${top(1)};`); + emit(`if(${top(3)}===null)np(${pc});a=${top(3)}.a;if(${top(2)}>>>0>=a.length)rt.arr(${top(3)},${top(2)},${pc});a[${top(2)}]=${top(1)};`); break; case 0x53: emit(`rt.aastore(${top(2)},${top(1)},${top()},${pc});`); break; case 0x57: case 0x58: break; @@ -889,7 +995,8 @@ function insn(g: Gen, b: Body, pc: number) { break; } const pop = m.isSynchronized ? "H.popFrame(t,f);" : "t.frames.pop();"; - emit(`${pop}if(f.direct){t.ret=${v};return 0;}H.deliver(t,${v},${JSON.stringify(m.ret)},false);return 0;`); + // deliver only tells void, boolean and wide results apart + emit(`${pop}if(f.direct){t.ret=${v};return 0;}H.deliver(t,${v},"${m.ret[0] === "[" ? "L" : m.ret[0]}",false);return 0;`); break; } case 0xb2: case 0xb3: { @@ -903,19 +1010,21 @@ function insn(g: Gen, b: Body, pc: number) { case 0xb4: case 0xb5: { const fld = cls.rcp[idx2] as JField | undefined; if (!fld) { emit(slow(pc, d)); break; } - if (op === 0xb4) emit(`if(${top()}===null)rt.npe(${pc});${top()}=${top()}.f[${fld.slot}];`); + if (op === 0xb4) emit(`${b.nn.has(d - 1) ? "" : `if(${top()}===null)np(${pc});`}${top()}=${top()}.f[${fld.slot}];`); else { - const obj = fld.wide ? top(2) : top(1); - emit(`if(${obj}===null)rt.npe(${pc});${obj}.f[${fld.slot}]=${fld.wide ? top(1) : top()};`); + const k = fld.wide ? 2 : 1; + const obj = top(k); + emit(`${b.nn.has(d - 1 - k) ? "" : `if(${obj}===null)np(${pc});`}${obj}.f[${fld.slot}]=${fld.wide ? top(1) : top()};`); } break; } case 0xb6: case 0xb7: case 0xb8: case 0xb9: { const target = cls.rcp[idx2] as JMethod | undefined; - if (region) { emit(regionInvoke(g, b, op, pc, d, target)); break; } + const known = !!target && op !== 0xb8 && b.nn.has(d - target.argSlots); + if (region) { emit(regionInvoke(g, b, op, pc, d, target, known)); break; } if (!target || (op === 0xb8 && target.cls.state !== INITIALIZED)) { emit(slow(pc, d)); break; } if (target.polymorphic) emit(genPolymorphic(g, pc, next, d, target)); - else emit(genInvoke(g, op, pc, next, d, target, cls)); + else emit(genInvoke(g, op, pc, next, d, target, cls, d, true, known)); break; } case 0xba: { @@ -949,10 +1058,10 @@ function insn(g: Gen, b: Body, pc: number) { emit(`${top()}=rt.newarray(${K(vm.arrayOf(k))},"L",${top()},${pc});`); break; } - case 0xbe: emit(`if(${top()}===null)rt.npe(${pc});${top()}=${top()}.a.length;`); break; + case 0xbe: emit(`if(${top()}===null)np(${pc});${top()}=${top()}.a.length;`); break; case 0xbf: if (region) throw new NoInline(); - emit(`if(${top()}===null)rt.npe(${pc});throw new rt.JavaThrow(${top()});`); + emit(`if(${top()}===null)np(${pc});throw new rt.JavaThrow(${top()});`); break; case 0xc0: case 0xc1: { const k = cls.rcp[idx2] as JClass | undefined; @@ -968,11 +1077,11 @@ function insn(g: Gen, b: Body, pc: number) { } case 0xc2: if (region) throw new NoInline(); - emit(`if(${top()}===null)rt.npe(${pc});if(!vm.monitorEnter(t,${top()}))${g.exit(pc, d, EXIT_SUSPEND)}`); + emit(`if(${top()}===null)np(${pc});if(!vm.monitorEnter(t,${top()}))${g.exit(pc, d, EXIT_SUSPEND)}`); break; case 0xc3: if (region) throw new NoInline(); - emit(`if(${top()}===null)rt.npe(${pc});vm.monitorExit(t,${top()});`); + emit(`if(${top()}===null)np(${pc});vm.monitorExit(t,${top()});`); break; case 0xc4: { const wop = c[pc + 1], i = u16(c, pc + 2); @@ -1031,7 +1140,7 @@ function fuseConcat(g: Gen, b: Body, pc: number): { pieces: string[]; end: numbe // An invoke inside a region: the target must be fixed and inlinable itself, or a // virtual call to the receiver class seen last here, which deoptimizes on any other. -function regionInvoke(g: Gen, b: Body, op: number, pc: number, d: number, target: JMethod | undefined): string { +function regionInvoke(g: Gen, b: Body, op: number, pc: number, d: number, target: JMethod | undefined, known: boolean): string { if (!target || target.polymorphic || (op === 0xb8 && target.cls.state !== INITIALIZED)) throw new NoInline(); let callee = op === 0xb7 ? g.t.vm.specialTarget(b.m.cls, target) : target; let guard: JClass | null = null; @@ -1046,10 +1155,11 @@ function regionInvoke(g: Gen, b: Body, op: number, pc: number, d: number, target const base = d - n; const args = Array.from({ length: n }, (_, i) => b.S(base + i)); const result = target.ret === "V" ? null : b.S(base); - const check = guard === null ? "" : `if(${args[0]}!==null&&${args[0]}.cls!==${g.K(guard)}){dpc=${pc};dsp=${d};throw rt.DEOPT;}`; + const check = guard === null ? "" : `if(${known ? "" : `${args[0]}!==null&&`}${args[0]}.cls!==${g.K(guard)}){dpc=${pc};dsp=${d};throw rt.DEOPT;}`; const trivial = trivialBody(callee); - if (trivial) return check + genInline(trivial, result ?? "v", args, pc, callee.isStatic); - return check + (callee.isStatic ? "" : `if(${args[0]}===null)rt.npe(${pc});`) + genRegion(g, b, callee, args, result, pc, base); + const needNull = !callee.isStatic && !known; + if (trivial) return check + genInline(trivial, result ?? "v", args, pc, needNull); + return check + (needNull ? `if(${args[0]}===null)np(${pc});` : "") + genRegion(g, b, callee, args, result, pc, base); } // The callee of an invoke does not depend on the receiver's class. @@ -1176,8 +1286,8 @@ function trivialBody(m: JMethod): Trivial | null { return build ? { kind: "expr", build } : null; } -function genInline(body: Trivial, res: string, args: string[], pc: number, isStatic: boolean): string { - const nullCheck = isStatic ? "" : `if(${args[0]}===null)rt.npe(${pc});`; +function genInline(body: Trivial, res: string, args: string[], pc: number, checkNull: boolean): string { + const nullCheck = checkNull ? `if(${args[0]}===null)np(${pc});` : ""; switch (body.kind) { case "empty": return nullCheck; case "const": return `${nullCheck}${res}=${body.value};`; @@ -1189,7 +1299,7 @@ function genInline(body: Trivial, res: string, args: string[], pc: number, isSta function genInvoke( g: Gen, op: number, pc: number, next: number, d: number, target: JMethod, cls: JClass, - slowDepth = d, inline = slowDepth === d, + slowDepth = d, inline = slowDepth === d, known = false, ): string { const { t, K, exit, call } = g; const n = target.argSlots; @@ -1213,11 +1323,13 @@ function genInvoke( const res = `s${base}`; let code: string | null = null; const trivial = trivialBody(callee); + // a class guard already rules out null + const needNull = !callee.isStatic && guard === null && !known; if (op === 0xb9 && !seen) code = null; - else if (trivial) code = genInline(trivial, res, args, pc, callee.isStatic); + else if (trivial) code = genInline(trivial, res, args, pc, needNull); else if (callee.code && !callee.isAbstract) { try { - const nullCheck = callee.isStatic ? "" : `if(${args[0]}===null)rt.npe(${pc});`; + const nullCheck = needNull ? `if(${args[0]}===null)np(${pc});` : ""; code = nullCheck + genRegion(g, g.top, callee, args, target.ret === "V" ? null : res, pc, base); } catch (e) { if (!(e instanceof NoInline)) throw e; @@ -1225,20 +1337,20 @@ function genInvoke( } if (code !== null) { if (guard === null) return code; - return `if(${args[0]}!==null&&${args[0]}.cls===${K(guard)}){${code}}else{${genInvoke(g, op, pc, next, d, target, cls, d, false)}}`; + return `if(${args[0]}!==null&&${args[0]}.cls===${K(guard)}){${code}}else{${genGuardMiss(g, pc, next, d, target)}}`; } } const parts: string[] = [`f.pc=${pc};`]; // pick the method if (op === 0xb6 || op === 0xb9) { - if (target.isPrivate) parts.push(`tm=${K(target)};if(${args[0]}===null)rt.npe(${pc});`); + if (target.isPrivate) parts.push(`tm=${K(target)};${known ? "" : `if(${args[0]}===null)np(${pc});`}`); else { const cell = K([null, null, target.key]); parts.push(`ic=${cell};tm=${args[0]}!==null&&${args[0]}.cls===ic[0]?ic[1]:rt.select(${args[0]},ic,${pc});`); } } else if (op === 0xb7) { const special = t.vm.specialTarget(cls, target); - parts.push(`tm=${K(special)};if(${args[0]}===null)rt.npe(${pc});`); + parts.push(`tm=${K(special)};${known ? "" : `if(${args[0]}===null)np(${pc});`}`); } else parts.push(`tm=${K(target)};`); const ret = target.ret; const res = (v: string) => (ret === "V" ? "" : `s${base}=${v};`); @@ -1248,12 +1360,26 @@ function genInvoke( parts.push(`r=tm.jit(t,cf${args.map((x) => "," + x).join("")});if(r!==0){t.unpool(cf);${exit(pc, base, EXIT_SUSPEND)}}`); parts.push(`${res("t.ret")}}`); // a native callee runs here; anything else goes through the interpreter - parts.push(`else{v=rt.slowCall(t,tm,[${nativeArgs(target, args)}]);`); - parts.push(`if(v===rt.PUSHED)${exit(pc, base, EXIT_SUSPEND)}if(v===rt.INTERPRET)${call(pc, slowDepth)}`); + const xp = g.xcode(pc, base, EXIT_SUSPEND), xi = g.xcode(pc, slowDepth, EXIT_INTERPRET); + parts.push(`else{v=rt.slowCall(t,tm,[${nativeArgs(target, args)}],${xp},${xi});if(v===rt.E){x=rt.xc;break X;}`); parts.push(`${res("v")}if(t.state!==0)${exit(next, ret === "V" ? base : base + (isWide(ret) ? 2 : 1), EXIT_SUSPEND)}}`); return parts.join(""); } +// A virtual or interface call after its inlining guard failed: the same +// call as genInvoke makes, out of line to keep the code small. +function genGuardMiss(g: Gen, pc: number, next: number, d: number, target: JMethod): string { + const n = target.argSlots; + const base = d - n; + const args = Array.from({ length: n }, (_, i) => `s${base + i}`); + const ret = target.ret; + const xp = g.xcode(pc, base, EXIT_SUSPEND), xi = g.xcode(pc, d, EXIT_INTERPRET); + const cell = g.K([null, null, target.key]); + const call = n <= 4 ? `rt.callv(t,f,${cell},${pc},${xp},${xi},${args})` : `rt.callvMany(t,f,${cell},[${args}],${pc},${xp},${xi})`; + return `v=${call};if(v===rt.E){x=rt.xc;break X;}${ret === "V" ? "" : `s${base}=v;`}` + + `if(t.state!==0)${g.exit(next, ret === "V" ? base : base + (isWide(ret) ? 2 : 1), EXIT_SUSPEND)}`; +} + // Signature-polymorphic calls, dispatched as the interpreter does: invokeBasic // to the handle's LambdaForm entry, linkTo* to the trailing MemberName, and // the rest (MethodHandle.invoke, VarHandle.get, ...) to the invoker linked for diff --git a/src/natives/misc.ts b/src/natives/misc.ts index e2fe900..c7b280f 100644 --- a/src/natives/misc.ts +++ b/src/natives/misc.ts @@ -30,6 +30,55 @@ function view(a: JArray): DataView { return (a.x = new DataView(d.buffer, d.byteOffset, d.byteLength)); } +// A byte[] access of another width, composed from bytes: a DataView per +// array would cost more than the short-lived arrays it serves. +const scratchBytes = new Uint8Array(8); +const scratch = new DataView(scratchBytes.buffer); + +function bytesRead(a: Int8Array, p: number, k: string, le: boolean): any { + switch (k) { + case "S": return le ? ((a[p + 1] << 24) | ((a[p] & 255) << 16)) >> 16 : ((a[p] << 24) | ((a[p + 1] & 255) << 16)) >> 16; + case "C": return le ? ((a[p + 1] & 255) << 8) | (a[p] & 255) : ((a[p] & 255) << 8) | (a[p + 1] & 255); + case "I": return le + ? (a[p + 3] << 24) | ((a[p + 2] & 255) << 16) | ((a[p + 1] & 255) << 8) | (a[p] & 255) + : (a[p] << 24) | ((a[p + 1] & 255) << 16) | ((a[p + 2] & 255) << 8) | (a[p + 3] & 255); + case "Z": return a[p] !== 0 ? 1 : 0; + case "B": return a[p]; + } + const n = k === "F" ? 4 : 8; + for (let i = 0; i < n; i++) scratchBytes[i] = a[p + i]; + return dvRead(scratch, 0, k, le); +} + +function bytesWrite(a: Int8Array, p: number, k: string, v: any, le: boolean) { + switch (k) { + case "S": case "C": + a[p + (le ? 0 : 1)] = v; + a[p + (le ? 1 : 0)] = v >> 8; + return; + case "I": + if (le) { a[p] = v; a[p + 1] = v >> 8; a[p + 2] = v >> 16; a[p + 3] = v >> 24; } + else { a[p + 3] = v; a[p + 2] = v >> 8; a[p + 1] = v >> 16; a[p] = v >> 24; } + return; + case "Z": case "B": a[p] = v; return; + } + dvWrite(scratch, 0, k, v, le); + const n = k === "F" ? 4 : 8; + for (let i = 0; i < n; i++) a[p + i] = scratchBytes[i]; +} + +// a primitive array element of another width, as the VM's byte order lays it out +function arrayRead(a: JArray, p: number, k: string, le = true): any { + const d = a.a as Int8Array; + return a.cls.component!.prim === "B" || a.cls.component!.prim === "Z" ? bytesRead(d, p, k, le) : dvRead(view(a), p, k, le); +} + +function arrayWrite(a: JArray, p: number, k: string, v: any, le = true) { + const d = a.a as Int8Array; + if (a.cls.component!.prim === "B" || a.cls.component!.prim === "Z") bytesWrite(d, p, k, v, le); + else dvWrite(view(a), p, k, v, le); +} + // native order is little-endian function dvRead(dv: DataView, p: number, k: string, le = true): any { switch (k) { @@ -71,7 +120,7 @@ export function unsafeGet(t: JThread, o: JObject | null, offset: bigint, k: stri const elem = o.cls.component!.prim ?? "L"; if (elem === "L") return o.a[(off - ARRAY_BASE_OFFSET) / REF_SCALE]; if (sameKind(elem, k)) return o.a[(off - ARRAY_BASE_OFFSET) / WIDTH[elem]]; - return dvRead(view(o), off - ARRAY_BASE_OFFSET, k); + return arrayRead(o, off - ARRAY_BASE_OFFSET, k); } if (off >= STATIC_OFFSET_BASE && o.x instanceof JClass) return o.x.statics[off - STATIC_OFFSET_BASE]; return o.f[off]; @@ -87,7 +136,7 @@ export function unsafePut(t: JThread, o: JObject | null, offset: bigint, k: stri const elem = o.cls.component!.prim ?? "L"; if (elem === "L") o.a[(off - ARRAY_BASE_OFFSET) / REF_SCALE] = v; else if (sameKind(elem, k)) (o.a as any)[(off - ARRAY_BASE_OFFSET) / WIDTH[elem]] = v; - else dvWrite(view(o), off - ARRAY_BASE_OFFSET, k, v); + else arrayWrite(o, off - ARRAY_BASE_OFFSET, k, v); return; } if (off >= STATIC_OFFSET_BASE && o.x instanceof JClass) o.x.statics[off - STATIC_OFFSET_BASE] = v; @@ -141,6 +190,38 @@ export const intrinsics: Natives = { [`${U}compareAndExchangeShort(Ljava/lang/Object;JSS)S`]: (t, [, o, off, e, v]) => cas(t, o, off, "S", e, v), }; +// The ordered accessors and read-modify-write methods that Unsafe builds from +// the volatile ones, as natives like HotSpot's intrinsics: Java threads here +// never interleave inside a native, so each is a plain read and write. +for (const [name, k] of KINDS) { + const desc = k === "L" ? "Ljava/lang/Object;" : k; + for (const suffix of ["Acquire", "Opaque"]) intrinsics[`${U}get${name}${suffix}(Ljava/lang/Object;J)${desc}`] = (t, [, o, off]) => unsafeGet(t, o, off, k); + for (const suffix of ["Release", "Opaque"]) intrinsics[`${U}put${name}${suffix}(Ljava/lang/Object;J${desc})V`] = (t, [, o, off, v]) => unsafePut(t, o, off, k, v); +} +for (const [name, k, desc] of [["Int", "I", "I"], ["Long", "J", "J"], ["Reference", "L", "Ljava/lang/Object;"]]) { + for (const suffix of ["Plain", "Acquire", "Release", ""]) { + intrinsics[`${U}weakCompareAndSet${name}${suffix}(Ljava/lang/Object;J${desc}${desc})Z`] = + (t, [, o, off, e, v]) => cas(t, o, off, k, e, v) === e; + } + for (const suffix of ["Acquire", "Release"]) { + intrinsics[`${U}compareAndExchange${name}${suffix}(Ljava/lang/Object;J${desc}${desc})${desc}`] = + (t, [, o, off, e, v]) => cas(t, o, off, k, e, v); + } + for (const suffix of ["", "Acquire", "Release"]) { + intrinsics[`${U}getAndSet${name}${suffix}(Ljava/lang/Object;J${desc})${desc}`] = (t, [, o, off, x]) => { + const old = unsafeGet(t, o, off, k); + unsafePut(t, o, off, k, x); + return old; + }; + if (k === "L") continue; + intrinsics[`${U}getAndAdd${name}${suffix}(Ljava/lang/Object;J${desc})${desc}`] = (t, [, o, off, d]) => { + const old = unsafeGet(t, o, off, k); + unsafePut(t, o, off, k, k === "I" ? (old + d) | 0 : BigInt.asIntN(64, old + d)); + return old; + }; + } +} + // Unaligned access, intrinsics in HotSpot too: one native-order read or write // instead of the Java version's byte-by-byte assembly. // a primitive array access that the element-indexed path cannot do @@ -160,12 +241,12 @@ function convEndian(k: string, v: any, bigEndian: boolean): any { for (const [name, k] of [["Long", "J"], ["Int", "I"], ["Short", "S"], ["Char", "C"]]) { const get = (t: JThread, o: JObject | null, offset: bigint, bigEndian: boolean) => { const off = Number(offset); - if (unaligned(o, off, k)) return dvRead(view(o), off - ARRAY_BASE_OFFSET, k, !bigEndian); + if (unaligned(o, off, k)) return arrayRead(o, off - ARRAY_BASE_OFFSET, k, !bigEndian); return convEndian(k, unsafeGet(t, o, offset, k), bigEndian); }; const put = (t: JThread, o: JObject | null, offset: bigint, v: any, bigEndian: boolean) => { const off = Number(offset); - if (unaligned(o, off, k)) dvWrite(view(o), off - ARRAY_BASE_OFFSET, k, v, !bigEndian); + if (unaligned(o, off, k)) arrayWrite(o, off - ARRAY_BASE_OFFSET, k, v, !bigEndian); else unsafePut(t, o, offset, k, convEndian(k, v, bigEndian)); }; intrinsics[`${U}get${name}Unaligned(Ljava/lang/Object;J)${k}`] = (t, [, o, off]) => get(t, o, off, false); diff --git a/src/runtime.ts b/src/runtime.ts index cfdb205..d8e114c 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -5,6 +5,7 @@ // null. Long and double take two stack and local slots like in the JVMS; the // second slot holds undefined. +import { memberAttrs } from "./classfile.ts"; import type { ClassFile, Code, Member } from "./classfile.ts"; import { PendingThrow } from "./thread.ts"; import type { JThread } from "./thread.ts"; @@ -76,7 +77,7 @@ export class JField { readonly name: string; readonly desc: string; readonly access: number; - readonly attrs: Map; + private attrsRaw: Map | number; readonly wide: boolean; slot = -1; index = 0; @@ -85,9 +86,13 @@ export class JField { this.name = m.name; this.desc = m.desc; this.access = m.access; - this.attrs = m.attrs; + this.attrsRaw = m.attrs; this.wide = isWide(m.desc); } + get attrs(): Map { + const a = this.attrsRaw; + return typeof a === "number" ? this.attrsRaw = memberAttrs(this.cls.cf!.cp, a) : a; + } get isStatic() { return (this.access & ACC_STATIC) !== 0; } } @@ -106,7 +111,7 @@ export class JMethod { readonly desc: string; readonly key: string; readonly access: number; - readonly attrs: Map; + private attrsRaw: Map | number; readonly code: Code | null; readonly params: string[]; readonly ret: string; @@ -134,7 +139,7 @@ export class JMethod { this.desc = m.desc; this.key = m.name + m.desc; this.access = m.access; - this.attrs = m.attrs; + this.attrsRaw = m.attrs; this.code = m.code; this.isStatic = (m.access & ACC_STATIC) !== 0; this.isNative = (m.access & ACC_NATIVE) !== 0; @@ -152,6 +157,10 @@ export class JMethod { readonly isAbstract: boolean; readonly isPrivate: boolean; readonly isSynchronized: boolean; + get attrs(): Map { + const a = this.attrsRaw; + return typeof a === "number" ? this.attrsRaw = memberAttrs(this.cls.cf!.cp, a) : a; + } toString() { return `${this.cls.name}.${this.name}${this.desc}`; } lineAt(pc: number): number { const lines = this.code?.lines; diff --git a/test/java/ByteArrayViews.java b/test/java/ByteArrayViews.java new file mode 100644 index 0000000..4e573fa --- /dev/null +++ b/test/java/ByteArrayViews.java @@ -0,0 +1,77 @@ +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.UUID; + +public class ByteArrayViews { + static final VarHandle SHORT_LE = MethodHandles.byteArrayViewVarHandle(short[].class, ByteOrder.LITTLE_ENDIAN); + static final VarHandle SHORT_BE = MethodHandles.byteArrayViewVarHandle(short[].class, ByteOrder.BIG_ENDIAN); + static final VarHandle CHAR_LE = MethodHandles.byteArrayViewVarHandle(char[].class, ByteOrder.LITTLE_ENDIAN); + static final VarHandle CHAR_BE = MethodHandles.byteArrayViewVarHandle(char[].class, ByteOrder.BIG_ENDIAN); + static final VarHandle INT_LE = MethodHandles.byteArrayViewVarHandle(int[].class, ByteOrder.LITTLE_ENDIAN); + static final VarHandle INT_BE = MethodHandles.byteArrayViewVarHandle(int[].class, ByteOrder.BIG_ENDIAN); + static final VarHandle LONG_LE = MethodHandles.byteArrayViewVarHandle(long[].class, ByteOrder.LITTLE_ENDIAN); + static final VarHandle LONG_BE = MethodHandles.byteArrayViewVarHandle(long[].class, ByteOrder.BIG_ENDIAN); + static final VarHandle FLOAT_LE = MethodHandles.byteArrayViewVarHandle(float[].class, ByteOrder.LITTLE_ENDIAN); + static final VarHandle DOUBLE_BE = MethodHandles.byteArrayViewVarHandle(double[].class, ByteOrder.BIG_ENDIAN); + + public static void main(String[] args) { + byte[] a = new byte[64]; + for (int i = 0; i < a.length; i++) a[i] = (byte) (i * 37 + 11); + long sum = 0; + for (int off = 0; off < 40; off++) { + sum = sum * 31 + (short) SHORT_LE.get(a, off); + sum = sum * 31 + (short) SHORT_BE.get(a, off); + sum = sum * 31 + (char) CHAR_LE.get(a, off); + sum = sum * 31 + (char) CHAR_BE.get(a, off); + sum = sum * 31 + (int) INT_LE.get(a, off); + sum = sum * 31 + (int) INT_BE.get(a, off); + sum = sum * 31 + (long) LONG_LE.get(a, off); + sum = sum * 31 + (long) LONG_BE.get(a, off); + sum = sum * 31 + Float.floatToRawIntBits((float) FLOAT_LE.get(a, off)); + sum = sum * 31 + Double.doubleToRawLongBits((double) DOUBLE_BE.get(a, off)); + } + System.out.println(sum); + + byte[] b = new byte[64]; + for (int off = 0; off < 40; off++) { + SHORT_LE.set(b, off, (short) (0x8001 + off * 257)); + SHORT_BE.set(b, off + 2, (short) (0xfe02 - off)); + CHAR_LE.set(b, off + 3, (char) (0xff01 - off)); + CHAR_BE.set(b, off + 5, (char) (0x7f80 + off)); + INT_LE.set(b, off + 7, 0x80000001 + off * 65537); + INT_BE.set(b, off + 11, 0x7ffffffe - off * 16777217); + LONG_LE.set(b, off + 13, 0x8000000000000001L + off * 0x0101010101010101L); + LONG_BE.set(b, off + 17, 0x7ffffffffffffffeL - off * 0x0100010001000101L); + FLOAT_LE.set(b, off + 19, 1.5f * off - 20f); + DOUBLE_BE.set(b, off + 23, 1e10 * off - 3.25); + System.out.println(Arrays.hashCode(b)); + } + + ByteBuffer bb = ByteBuffer.wrap(new byte[48]); + for (ByteOrder order : new ByteOrder[] { ByteOrder.BIG_ENDIAN, ByteOrder.LITTLE_ENDIAN }) { + bb.order(order); + for (int off = 1; off < 30; off += 3) { + bb.putShort(off, (short) (off * 1111)); + bb.putChar(off + 2, (char) (off * 2222)); + bb.putInt(off + 4, off * 33333333); + bb.putLong(off + 8, off * 4444444444444L); + bb.putFloat(off + 16, off * 0.1f); + bb.putDouble(off + 20, off * 0.01); + System.out.println(bb.getShort(off) + " " + (int) bb.getChar(off + 2) + " " + bb.getInt(off + 4) + " " + bb.getLong(off + 8) + " " + bb.getFloat(off + 16) + " " + bb.getDouble(off + 20)); + } + System.out.println(Arrays.hashCode(bb.array())); + } + + int[] ints = new int[8]; + long[] longs = new long[4]; + ByteBuffer.wrap(new byte[32]).asIntBuffer().put(ints); + ByteBuffer lb = ByteBuffer.allocate(32); + lb.asLongBuffer().put(longs); + + UUID u = new UUID(0x0123456789abcdefL, 0xfedcba9876543210L); + System.out.println(u + " " + UUID.nameUUIDFromBytes(a) + " " + UUID.fromString(u.toString()).equals(u)); + } +} diff --git a/test/java/JitGuardMiss.java b/test/java/JitGuardMiss.java new file mode 100644 index 0000000..4aa84aa --- /dev/null +++ b/test/java/JitGuardMiss.java @@ -0,0 +1,117 @@ +import java.util.function.IntSupplier; + +public class JitGuardMiss { + interface Shape { + long area(long k, double d); + double ratio(float f); + boolean big(int n); + int[] cells(int n); + String name(); + void touch(int[] box); + } + + static final class Sq implements Shape { + final long s; + Sq(long s) { this.s = s; } + public long area(long k, double d) { return s * s + k + (long) d; } + public double ratio(float f) { return s * 0.5 + f; } + public boolean big(int n) { return s > n; } + public int[] cells(int n) { return new int[] { n, (int) s }; } + public String name() { return "sq" + s; } + public void touch(int[] box) { box[0] += (int) s; } + } + + static final class Circ implements Shape { + final long r; + Circ(long r) { this.r = r; } + public long area(long k, double d) { return 3 * r * r - k + (long) (d * 2); } + public double ratio(float f) { return r * 1.5 - f; } + public boolean big(int n) { return r * 2 > n; } + public int[] cells(int n) { return new int[] { n + 1, (int) r, 7 }; } + public String name() { return "circ" + r; } + public void touch(int[] box) { box[0] -= (int) r; } + } + + static final class Tri implements Shape { + public long area(long k, double d) { throw new IllegalStateException("tri " + k); } + public double ratio(float f) { return f; } + public boolean big(int n) { return false; } + public int[] cells(int n) { return new int[0]; } + public String name() { return "tri"; } + public void touch(int[] box) { } + } + + static class Base { + int v; + long w; + Base(int v) { this.v = v; } + int get() { return v; } + void set(int x) { v = x; w += x; } + int twice() { return get() + get(); } + } + + static long total(Shape s, long k) { + long t = s.area(k, 1.5); + t += (long) s.ratio(2f); + if (s.big(10)) t += 1; + t += s.cells(3).length; + t += s.name().length(); + int[] box = new int[1]; + s.touch(box); + return t + box[0]; + } + + static int via(Base b) { return b.twice(); } + + static void run(String label, Shape[] shapes, int rounds) { + long sum = 0; + for (int i = 0; i < rounds; i++) sum += total(shapes[i % shapes.length], i); + System.out.println(label + " " + sum); + } + + public static void main(String[] args) { + Shape[] sq = { new Sq(4) }; + Shape[] mix = { new Sq(4), new Circ(3), new Sq(5), new Circ(2) }; + run("warm", sq, 3000); + run("miss", mix, 3000); + run("again", sq, 3000); + + Shape[] withTri = { new Sq(2), new Tri() }; + try { + run("tri", withTri, 3000); + } catch (IllegalStateException e) { + System.out.println(e.getMessage()); + } + + int nulls = 0; + for (int i = 0; i < 2000; i++) { + try { + total(i % 100 == 99 ? null : sq[0], i); + } catch (NullPointerException e) { + nulls++; + } + } + System.out.println("npe " + nulls); + + Base b = new Base(5); + long t = 0; + for (int i = 0; i < 4000; i++) { + b.set(i); + t += via(b); + } + System.out.println(t + " " + b.w); + int caught = 0; + for (int i = 0; i < 1000; i++) { + try { + via(i % 10 == 9 ? null : b); + } catch (NullPointerException e) { + caught++; + } + } + System.out.println("npe2 " + caught); + IntSupplier sup = b::get; + long s2 = 0; + for (int i = 0; i < 2000; i++) s2 += sup.getAsInt(); + System.out.println(s2); + } +} diff --git a/test/java/ReflectAttrs.java b/test/java/ReflectAttrs.java new file mode 100644 index 0000000..33f7c82 --- /dev/null +++ b/test/java/ReflectAttrs.java @@ -0,0 +1,82 @@ +// javac: -g -parameters +import java.io.IOException; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +public class ReflectAttrs { + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) + @interface Tag { + String value() default "none"; + int level() default 3; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE_USE) + @interface Use { } + + static class Box> { + @Tag("field") public Map> items; + public @Use String used; + public int plain; + + @Tag(level = 7) public List pick(@Tag("p") Map from, int count, T[] more) throws IOException, IllegalStateException { + return null; + } + + public @Use String twice(@Use String s) { return s + s; } + + @Deprecated + public void old() { } + } + + static String describe(Method m) { + StringBuilder sb = new StringBuilder(m.getName()); + sb.append(" ret=").append(m.getGenericReturnType()); + sb.append(" params=").append(Arrays.toString(m.getGenericParameterTypes())); + sb.append(" throws=").append(Arrays.toString(m.getExceptionTypes())); + sb.append(" ann=").append(Arrays.toString(m.getAnnotations())); + for (Parameter p : m.getParameters()) { + sb.append(" [").append(p.getName()).append(' ').append(Arrays.toString(p.getAnnotations())).append(']'); + } + sb.append(" typeUse=").append(m.getAnnotatedReturnType().getAnnotations().length); + return sb.toString(); + } + + static int length(String text) { + String local = text; + return local.length(); + } + + public static void main(String[] args) throws Exception { + Method[] ms = Box.class.getDeclaredMethods(); + Arrays.sort(ms, Comparator.comparing(Method::getName)); + for (Method m : ms) System.out.println(describe(m)); + Field[] fs = Box.class.getDeclaredFields(); + Arrays.sort(fs, Comparator.comparing(Field::getName)); + for (Field f : fs) { + System.out.println(f.getName() + " " + f.getGenericType() + " " + Arrays.toString(f.getAnnotations()) + " " + f.getAnnotatedType().getAnnotations().length); + } + System.out.println(Tag.class.getDeclaredMethod("value").getDefaultValue()); + try { + length(null); + } catch (NullPointerException e) { + System.out.println(e.getMessage()); + } + try { + String a = args.length > 5 ? "x" : null; + System.out.println(a.length()); + } catch (NullPointerException e) { + System.out.println(e.getMessage()); + } + } +} diff --git a/test/java/UnsafeAtomics.java b/test/java/UnsafeAtomics.java new file mode 100644 index 0000000..2f0c522 --- /dev/null +++ b/test/java/UnsafeAtomics.java @@ -0,0 +1,113 @@ +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicIntegerArray; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicLongArray; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.AtomicReferenceArray; +import java.util.concurrent.atomic.LongAdder; + +public class UnsafeAtomics { + static final class Cell { + int i = Integer.MAX_VALUE - 2; + long l = Long.MAX_VALUE - 2; + Object r = "start"; + boolean z; + byte b = 120; + short s = 32760; + char c = 65530; + float f = 1.5f; + double d = 2.5; + } + + static final VarHandle I, L, R, Z, B, S, C, F, D, IA, LA, RA; + static { + try { + MethodHandles.Lookup lk = MethodHandles.lookup(); + I = lk.findVarHandle(Cell.class, "i", int.class); + L = lk.findVarHandle(Cell.class, "l", long.class); + R = lk.findVarHandle(Cell.class, "r", Object.class); + Z = lk.findVarHandle(Cell.class, "z", boolean.class); + B = lk.findVarHandle(Cell.class, "b", byte.class); + S = lk.findVarHandle(Cell.class, "s", short.class); + C = lk.findVarHandle(Cell.class, "c", char.class); + F = lk.findVarHandle(Cell.class, "f", float.class); + D = lk.findVarHandle(Cell.class, "d", double.class); + IA = MethodHandles.arrayElementVarHandle(int[].class); + LA = MethodHandles.arrayElementVarHandle(long[].class); + RA = MethodHandles.arrayElementVarHandle(Object[].class); + } catch (ReflectiveOperationException e) { + throw new ExceptionInInitializerError(e); + } + } + + static void modes() { + Cell c = new Cell(); + System.out.println((int) I.getAndAdd(c, 5) + " " + (int) I.getAndAddAcquire(c, 1) + " " + (int) I.getAndAddRelease(c, -3) + " " + c.i); + System.out.println((long) L.getAndAdd(c, 5L) + " " + (long) L.getAndAddAcquire(c, 1L) + " " + (long) L.getAndAddRelease(c, -3L) + " " + c.l); + System.out.println((int) I.getAndSet(c, 7) + " " + (int) I.getAndSetAcquire(c, 8) + " " + (int) I.getAndSetRelease(c, 9) + " " + c.i); + System.out.println((long) L.getAndSet(c, 7L) + " " + (long) L.getAndSetAcquire(c, 8L) + " " + (long) L.getAndSetRelease(c, 9L) + " " + c.l); + System.out.println(R.getAndSet(c, "a") + " " + R.getAndSetAcquire(c, "b") + " " + R.getAndSetRelease(c, "c") + " " + c.r); + System.out.println(I.weakCompareAndSet(c, 9, 10) + " " + I.weakCompareAndSetPlain(c, 9, 11) + " " + I.weakCompareAndSetAcquire(c, 10, 12) + " " + I.weakCompareAndSetRelease(c, 12, 13) + " " + c.i); + System.out.println(L.weakCompareAndSet(c, 9L, 10L) + " " + L.weakCompareAndSetPlain(c, 9L, 11L) + " " + L.weakCompareAndSetAcquire(c, 10L, 12L) + " " + L.weakCompareAndSetRelease(c, 12L, 13L) + " " + c.l); + System.out.println(R.weakCompareAndSet(c, "c", "d") + " " + R.weakCompareAndSetPlain(c, "c", "e") + " " + R.weakCompareAndSetAcquire(c, "d", "f") + " " + R.weakCompareAndSetRelease(c, "f", "g") + " " + c.r); + System.out.println((int) I.compareAndExchange(c, 13, 14) + " " + (int) I.compareAndExchangeAcquire(c, 13, 15) + " " + (int) I.compareAndExchangeRelease(c, 14, 16) + " " + c.i); + System.out.println((long) L.compareAndExchange(c, 13L, 14L) + " " + (long) L.compareAndExchangeAcquire(c, 13L, 15L) + " " + (long) L.compareAndExchangeRelease(c, 14L, 16L) + " " + c.l); + System.out.println(R.compareAndExchange(c, "g", "h") + " " + R.compareAndExchangeAcquire(c, "g", "i") + " " + R.compareAndExchangeRelease(c, "h", "j") + " " + c.r); + + I.setRelease(c, 21); L.setRelease(c, 22L); R.setRelease(c, "rel"); Z.setRelease(c, true); B.setRelease(c, (byte) 23); S.setRelease(c, (short) 24); + C.setRelease(c, (char) 25); F.setRelease(c, 26.5f); D.setRelease(c, 27.5); + System.out.println((int) I.getAcquire(c) + " " + (long) L.getAcquire(c) + " " + R.getAcquire(c) + " " + (boolean) Z.getAcquire(c) + " " + (byte) B.getAcquire(c) + " " + (short) S.getAcquire(c) + + " " + (int) (char) C.getAcquire(c) + " " + (float) F.getAcquire(c) + " " + (double) D.getAcquire(c)); + I.setOpaque(c, 31); L.setOpaque(c, 32L); R.setOpaque(c, "opq"); Z.setOpaque(c, false); B.setOpaque(c, (byte) 33); S.setOpaque(c, (short) 34); + C.setOpaque(c, (char) 35); F.setOpaque(c, 36.5f); D.setOpaque(c, 37.5); + System.out.println((int) I.getOpaque(c) + " " + (long) L.getOpaque(c) + " " + R.getOpaque(c) + " " + (boolean) Z.getOpaque(c) + " " + (byte) B.getOpaque(c) + " " + (short) S.getOpaque(c) + + " " + (int) (char) C.getOpaque(c) + " " + (float) F.getOpaque(c) + " " + (double) D.getOpaque(c)); + + System.out.println((byte) B.getAndAdd(c, (byte) 100) + " " + (short) S.getAndAdd(c, (short) 40000) + " " + (int) (char) C.getAndAdd(c, (char) 70000)); + System.out.println(c.b + " " + c.s + " " + (int) c.c); + System.out.println((boolean) Z.getAndSet(c, true) + " " + (boolean) Z.getAndSetAcquire(c, false) + " " + (byte) B.getAndSet(c, (byte) 5) + " " + (short) S.getAndSetRelease(c, (short) 6)); + + int[] ia = new int[4]; + long[] la = new long[4]; + Object[] ra = new Object[4]; + System.out.println((int) IA.getAndAdd(ia, 2, 5) + " " + (int) IA.getAndAdd(ia, 2, Integer.MAX_VALUE) + " " + ia[2]); + System.out.println((long) LA.getAndAdd(la, 1, Long.MAX_VALUE) + " " + (long) LA.getAndAdd(la, 1, 5L) + " " + la[1]); + System.out.println(RA.getAndSet(ra, 3, "x") + " " + RA.compareAndSet(ra, 3, "x", "y") + " " + RA.getAcquire(ra, 3)); + } + + static void classes() { + AtomicInteger ai = new AtomicInteger(Integer.MAX_VALUE - 1); + AtomicLong al = new AtomicLong(Long.MAX_VALUE - 1); + AtomicReference ar = new AtomicReference<>("a"); + AtomicBoolean ab = new AtomicBoolean(); + LongAdder adder = new LongAdder(); + AtomicIntegerArray aia = new AtomicIntegerArray(8); + AtomicLongArray ala = new AtomicLongArray(8); + AtomicReferenceArray ara = new AtomicReferenceArray<>(8); + ConcurrentHashMap chm = new ConcurrentHashMap<>(); + long sum = 0; + for (int n = 0; n < 3000; n++) { + sum += ai.incrementAndGet() + ai.getAndAdd(n) + ai.addAndGet(-n) + ai.getAndSet(n) + ai.updateAndGet(x -> x * 3 + 1) + ai.accumulateAndGet(n, Math::max); + sum += al.incrementAndGet() + al.getAndAdd(n) + al.addAndGet(-n * 7L) + al.getAndSet(n) + al.updateAndGet(x -> x * 31 + 1) + al.accumulateAndGet(n, Math::max); + sum += ar.getAndSet("s" + (n % 5)).length() + (ar.compareAndSet("s1", "t") ? 1 : 0) + (ar.weakCompareAndSetPlain("t", "u") ? 2 : 0) + ar.getAcquire().length(); + sum += ab.getAndSet(n % 2 == 0) ? 1 : 0; + adder.add(n); + sum += aia.getAndAdd(n & 7, n) + aia.incrementAndGet(n & 7) + (aia.compareAndSet(n & 7, 5, 6) ? 1 : 0); + sum += ala.getAndAdd(n & 7, n) + ala.incrementAndGet(n & 7) + (ala.weakCompareAndSetPlain(n & 7, 5, 6) ? 1 : 0); + sum += (ara.getAndSet(n & 7, "e" + n) == null ? 1 : 0) + (ara.compareAndSet(n & 7, "e" + n, "f") ? 1 : 0); + chm.put(n % 100, "v" + n); + sum += chm.getOrDefault(n % 50, "").length() + chm.size(); + if (n % 7 == 0) chm.remove(n % 100); + } + System.out.println(sum + " " + ai + " " + al + " " + ar + " " + ab + " " + adder.sum() + " " + aia + " " + ala + " " + ara + " " + chm.size()); + } + + public static void main(String[] args) { + modes(); + classes(); + } +}