I thought I might make a centralized thread for performance testing current WIPS. I don’t have a lot of interesting things to say (yet), as I need to test out some things first. I can say that the startup for something I’m working on is a total hog at startup, as it does a lot of external file testing/reading, as well as some table stuff. Hopefully that’s just at startup…
Anyway, I’ll come back after I play around a bit, but I thought it might be helpful if people shared experiences with performance testing and tuning for Inform games. It could be a good resource for people searching the forum.
There’s actually a specific extension for speeding up those sorts of startup processes, if I can just find it…
It takes advantage of the fact that the save-game format is standardized in Glulx. When you’re releasing your game, you run the startup sequence once, then the game autosaves immediately afterward. Distribute that autosave along with your game, and it will be automatically loaded if it’s present, skipping the whole process.
Doesn’t necessarily help if those external files are generated by something outside your game, though. At least it will help with the table parts.
If the data is generated from the rest of your source code, have some code generate Inform code via printout and redirect output to a header file and have it feed back into your build system.
(Typically you build the game once with a special define set on the command line, run that to generate the header file, then build the game again without the special define set, which causes the data to come from the header file you just generated instead of the code you just ran)
Another option of course is have a tool written in a language of your choice that scans the input data and builds the header from that.
I’ve been playing around a bit with the CPU throttled (4x slowdown). The game is working hard! This is all before the command prompt, through there are some keypresses along the way. The first is the external file reads; that’s not so bad. The second bit is a bit more hungry. That’s down to setting properties from values in tables.
This a sort of roguelike game. There’s a lot of information that needs to get exchanged between playthroughs. I mean “a lot” in Inform terms, I guess, since the default is none at all. The frequency between turns is probably the bigger consideration. I haven’t gotten to that yet, though. I’ll look at that after I check out some of these table read/writes.
This is running on a year-old laptop, throttled down 4x. That second bump definitely feels like a long time. I haven’t looked yet, but I expect that the code is quite messy (I just wanted it to work in the beginning).
These are one-time events. I believe I can improve these numbers (especially the second one) but that’s probably just a warmup for the various things happening turn to turn.
Anyway, this is just random performance talk! This thread is open for anyone interested in managing performance in Inform.
What kinds of properties are you setting? If you’re dealing with a lot of texts and lists, I have some code here that should speed up block-value allocation.
Experimental Performance Improvements for Inform 10.1
Include (-
[ HeapMakeSpace size multiple newblocksize newblock B n hsize;
for (::) {
if (multiple) {
hsize = BLK_DATA_MULTI_OFFSET;
if (HeapNetFreeSpace(multiple) >= size) rtrue;
} else {
hsize = BLK_DATA_OFFSET;
if (HeapLargestFreeBlock(0) >= size) rtrue;
}
!newblocksize = 1;
!for (n=0: (n<SMALLEST_BLK_WORTH_ALLOCATING) || (newblocksize<(size+hsize)): n++)
! newblocksize = newblocksize*2;
n = SMALLEST_BLK_WORTH_ALLOCATING;
@shiftl 1 SMALLEST_BLK_WORTH_ALLOCATING newblocksize;
while (newblocksize < (size+hsize)) {
@shiftl newblocksize 1 newblocksize;
n = n+1;
}
! Could theoretically also do this instead of calculating n as we go:
! n = max(SMALLEST_BLK_WORTH_ALLOCATING, size+hsize);
! @shiftl 1 n newblocksize
newblock = VM_AllocateMemory(newblocksize);
if (newblock == 0) rfalse;
newblock->BLK_HEADER_N = n;
newblock-->BLK_HEADER_KOV = 0;
newblock-->BLK_HEADER_RCOUNT = 0;
newblock->BLK_HEADER_FLAGS = BLK_FLAG_MULTIPLE;
newblock-->BLK_NEXT = NULL;
newblock-->BLK_PREV = NULL;
for (B = Flex_Heap-->BLK_NEXT:B ~= NULL:B = B-->BLK_NEXT)
if (B-->BLK_NEXT == NULL) {
B-->BLK_NEXT = newblock;
newblock-->BLK_PREV = B;
jump Linked;
}
Flex_Heap-->BLK_NEXT = newblock;
newblock-->BLK_PREV = Flex_Heap;
.Linked; ;
#ifdef BLKVALUE_TRACE;
print "Increasing heap to free space map: "; FlexDebugDecomposition(Flex_Heap, 0);
#endif;
}
rtrue;
];
-) replacing "HeapMakeSpace".
Include (-
[ FlexAllocate size kov flags
dsize n m free_block min_m max_m smallest_oversized_block secondhalf i hsize head tail;
if (HeapMakeSpace(size, flags & BLK_FLAG_MULTIPLE) == false) FlexError("ran out");
! Calculate the header size for a block of this KOV
if (flags & BLK_FLAG_MULTIPLE) hsize = BLK_DATA_MULTI_OFFSET;
else hsize = BLK_DATA_OFFSET;
! Calculate the data size
!n=0; for (dsize=1: ((dsize < hsize+size) || (n<3+(WORDSIZE/2))): dsize=dsize*2) n++;
n = 3 + (WORDSIZE/2);
@shiftl 1 n dsize;
while (dsize < hsize+size) {
@shiftl dsize 1 dsize;
n++;
}
!n = VM_floorlog2int(hsize+size)+1;
!if (n<5) n = 5;
!@shiftl 1 n dsize;
! Seek a free block closest to the correct size, but starting from the
! block after the fixed head-free-block, which we can't touch
min_m = 10000; max_m = 0;
for (free_block = Flex_Heap-->BLK_NEXT:
free_block ~= NULL:
free_block = free_block-->BLK_NEXT) {
m = free_block->BLK_HEADER_N;
! Current block the ideal size
if (m == n) jump CorrectSizeFound;
! Current block too large: find the smallest which is larger than needed
if (m > n) {
if (min_m > m) {
min_m = m;
smallest_oversized_block = free_block;
}
}
! Current block too small: find the largest which is smaller than needed
if (m < n) {
if (max_m < m) {
max_m = m;
}
}
}
if (min_m == 10000) {
! Case I: No block is large enough to hold the entire size
if (flags & BLK_FLAG_MULTIPLE == 0) FlexError("too fragmented");
! Set dsize to the size in bytes if the largest block available
!for (dsize=1: max_m > 0: dsize=dsize*2) max_m--;
@shiftl 1 max_m dsize;
! Split as a head (dsize-hsize), which we can be sure fits into one block,
! plus a tail (size-(dsize-hsize), which might be a list of blocks
head = FlexAllocate(dsize-hsize, kov, flags);
if (head == 0) FlexError("for head block not available");
tail = FlexAllocate(size-(dsize-hsize), kov, flags);
if (tail == 0) FlexError("for tail block not available");
head-->BLK_NEXT = tail;
tail-->BLK_PREV = head;
return head;
}
! Case II: No block is the right size, but some exist which are too big
! Set dsize to the size in bytes of the smallest oversized block
!for (dsize=1,m=1: m<=min_m: dsize=dsize*2) m++;
@shiftl 1 min_m dsize;
m = min_m + 1;
free_block = smallest_oversized_block;
while (min_m > n) {
! Repeatedly halve free_block at the front until the two smallest
! fragments left are the correct size: then take the frontmost
!dsize = dsize/2;
@ushiftr dsize 1 dsize;
!print "Halving size to ", dsize, "^";
secondhalf = free_block + dsize;
secondhalf-->BLK_NEXT = free_block-->BLK_NEXT;
if (secondhalf-->BLK_NEXT ~= NULL)
(secondhalf-->BLK_NEXT)-->BLK_PREV = secondhalf;
secondhalf-->BLK_PREV = free_block;
free_block-->BLK_NEXT = secondhalf;
free_block->BLK_HEADER_N = (free_block->BLK_HEADER_N) - 1;
secondhalf->BLK_HEADER_N = free_block->BLK_HEADER_N;
secondhalf-->BLK_HEADER_KOV = free_block-->BLK_HEADER_KOV;
secondhalf-->BLK_HEADER_RCOUNT = 0;
secondhalf->BLK_HEADER_FLAGS = free_block->BLK_HEADER_FLAGS;
min_m--;
}
! Once that is done, free_block points to a block which is exactly the
! right size, so we can fall into...
! Case III: There is a free block which has the correct size.
.CorrectSizeFound;
! Delete the free block from the double linked list of free blocks: note
! that it cannot be the head of this list, which is fixed
if (free_block-->BLK_NEXT == NULL) {
! We remove final block, so previous is now final
(free_block-->BLK_PREV)-->BLK_NEXT = NULL;
} else {
! We remove a middle block, so join previous to next
(free_block-->BLK_PREV)-->BLK_NEXT = free_block-->BLK_NEXT;
(free_block-->BLK_NEXT)-->BLK_PREV = free_block-->BLK_PREV;
}
free_block-->BLK_HEADER_KOV = KindAtomic(kov);
free_block-->BLK_HEADER_RCOUNT = 1;
free_block->BLK_HEADER_FLAGS = flags;
if (flags & BLK_FLAG_MULTIPLE) {
free_block-->BLK_NEXT = NULL;
free_block-->BLK_PREV = NULL;
}
! Zero out the data bytes in the memory allocated
!for (i=hsize:i<dsize:i++) free_block->i=0;
m = free_block+hsize;
n = dsize-hsize;
@mzero n m;
return free_block;
];
-) replacing "FlexAllocate".
Include (-
[ FlexRecutInternal first last tsize backsize mfrom mto bnext backend n dsize fine_so_far;
if (first == last) rfalse;
mfrom = first; mto = last + FlexSize(last);
bnext = last-->BLK_NEXT;
fine_so_far = true;
for (:mto>mfrom: mto = mto - backsize) {
!for (n=0, backsize=1: backsize*2 <= mto-mfrom: n++) backsize=backsize*2;
n = VM_floorlog2int(mto-mfrom);
@shiftl 1 n backsize;
if ((fine_so_far) && (backsize == FlexSize(last))) {
bnext = last; last = last-->BLK_PREV;
bnext-->BLK_PREV = last;
last-->BLK_NEXT = bnext;
continue;
}
fine_so_far = false; ! From this point, "last" is meaningless
backend = mto - backsize;
backend->BLK_HEADER_N = n;
backend-->BLK_HEADER_KOV = 0;
backend-->BLK_HEADER_RCOUNT = 0;
backend->BLK_HEADER_FLAGS = BLK_FLAG_MULTIPLE;
backend-->BLK_NEXT = bnext;
if (bnext ~= NULL) {
bnext-->BLK_PREV = backend;
bnext = backend;
}
}
if (fine_so_far) rfalse;
rtrue;
];
-) replacing "FlexRecutInternal".
Include (-
[ FlexResize block req newsize dsize newblk kov n i flags;
if (block == 0) FlexError("failed resizing null block");
kov = block-->BLK_HEADER_KOV;
flags = block->BLK_HEADER_FLAGS;
if (flags & BLK_FLAG_MULTIPLE == 0) FlexError("failed resizing inextensible block");
newsize = req;
for (:: block = block-->BLK_NEXT) {
n = block->BLK_HEADER_N;
!for (dsize=1: n>0: n-- ) dsize = dsize*2;
@shiftl 1 n dsize;
i = dsize - BLK_DATA_MULTI_OFFSET;
newsize = newsize - i;
if (newsize > 0) {
if (block-->BLK_NEXT ~= NULL) continue;
newblk = FlexAllocate(newsize, kov, flags);
if (newblk == 0) rfalse;
block-->BLK_NEXT = newblk;
newblk-->BLK_PREV = block;
return;
}
if (block-->BLK_NEXT ~= NULL) {
FlexFree(block-->BLK_NEXT);
block-->BLK_NEXT = NULL;
}
return;
}
];
-) replacing "FlexResize".
Include (-
[ FlexSize txb bsize m; ! Size of an individual block, including header
if (txb == 0) return 0;
m = txb->BLK_HEADER_N;
!for (bsize=1: n<m: bsize=bsize*2) n++;
@shiftl 1 m bsize;
return bsize;
];
-) replacing "FlexSize".
Include (-
! Calculate floor(log2(m)) by counting the leading zeroes in the binary representation.
[ VM_floorlog2int m n c;
#ifdef DEBUG;
if (m == 0) {
print "** Programming error: VM_floorlog2int of zero.^";
@quit;
}
#endif; ! DEBUG
for (c=$80000000, n=0 : (m&c) == 0 : n = n+1)
@ushiftr c 1 c;
! If and when we get a count-leading-zeroes opcode in Glulx,
! the above loop can become something like this:
! @lzcnt m n;
return 31-n;
];
-).
(Edit: I should mention that this is for Glulx only.)
OK, yes! It does look like some property-setting rules have gotten better! Thanks for this.
There’s a bubble when the menus system (an extension) is built for the first time, but that’s one-time even that saves performance down the line.
I’ve noted that having a dynamic status line is fairly demanding, even if it’s something as simple as
when play begins:
now left hand status line is "[one of]1[or]2[or]3[cycling]";
but probably everything at 4x throttling is expensive. An empty project will occasionally blow past 200ms with a custom status line. At least on my laptop, which is perhaps a little underpowered.
Still, I’d like to optimize background tasks since there are so many of them. I’ll probably keep poking at this. I’m hoping to do some playtesting soon. Hopefully a variety of devices can be checked.
Does it help to cut out the middleman and use a custom rule for constructing the status line? I’m surprised there’s any real cost to it at all, honestly.
Sampling an empty project: over 40 enter presses I had an average timeof 134 (default status) vs 144 (custom status) to generate an “I beg your pardon?” response. In a real game, I would expect the gap to be higher. On the other hand, there’s so much potential drift with Windows systems because they are so often running background junk. Perhaps these numbers would be much better if I ran them again! Or worse.
10ms isn’t a lot in isolation, but because the throttled CPU performance is already chugging, it feels substantial. Still, there’s probably only so well a complex game with modern features can do on constrained hardware. I doubt many players would experience these gaps, but, since the game isn’t finished, there may be more properties and such to manage yet. Trying to whittle everything down makes sense.
Now that I have a reference, I can see what kind of difference (if any) the status window is making in the WIP.
To be fair, in an empty project that’s not doing any parsing (“I beg your pardon?” falls out of the parser at a very early stage) or any of the turn sequence (that only happens when an action happens), the game isn’t doing much of anything except printing the status line! Status line, prompt, “reading a command” activity, and that’s basically it. So I’m not actually surprised adding a small amount of complexity to the status line shows a noticeable difference in that case.
I picked the enter press for that reason! I thought it was interesting that perhaps the cost of a “[one of]…” was higher than printing the name of the location, not that it would matter in most cases.
But I need to go back to my “real” status line and see what’s pushing what.