LessonsExpert: BeyondLesson 59

Making code fast

Measure before you change anything, then the handful of habits that make xTalk fast: repeat for each, arrays for lookups, the built-in commands, variables instead of fields, and lock screen.

  • Expert
  • Lesson 59 of 83
  • About 10 minutes
  • 8 examples

Measure first

Most code is fast enough, and the slow part of a program is rarely where you would guess. So before changing anything for speed, measure: the milliseconds before and after (Timers and waiting), on data as big as the real thing. The examples here time two ways of doing the same work and check that the faster really is faster; each says how the two compared on the computer that checked this page.

ExampleRun it in the playground
function timeIt pHandler
   local tStart
   put the milliseconds into tStart
   dispatch pHandler to me
   return the milliseconds - tStart
end timeIt

on countToAMillion
   repeat with i = 1 to 1000000
   end repeat
end countToAMillion

put "a million empty loops:" && timeIt("countToAMillion") && "ms"
Output (yours will differ: it changes from run to run)
a million empty loops: 1139 ms

Going through lines: repeat for each

This is the biggest one. line i of tText has to count returns from the start of the text every time, so a loop over all the lines with it counts the first lines again and again: twice the lines takes four times as long. repeat for each line walks through the text once:

ExampleRun it in the playground
repeat with i = 1 to 10000
   put "line number" && i & return after tText
end repeat

put the milliseconds into tStart
repeat with i = 1 to the number of lines of tText
   get line i of tText
end repeat
put the milliseconds - tStart into tByNumber

put the milliseconds into tStart
repeat for each line tLine in tText
   get tLine
end repeat
put the milliseconds - tStart into tForEach

put "for each is at least 5 times faster:" && (tByNumber > 5 * max(1, tForEach))
Output
for each is at least 5 times faster: true

With 20,000 lines, the check took about 1700 milliseconds by number and 35 with for each. The same goes for item i, word i and char i. When you need the number too, count it yourself inside a for each loop. And put the text into an array with split when you must jump to any line by its number many times (Algorithms).

Looking things up: arrays

is among the lines of, lineOffset and contains search from the start each time. To ask "have I seen this?" many times, keep the things as the keys of an array: finding a key takes the same short time however big the array is.

ExampleRun it in the playground
repeat with i = 1 to 5000
   put "user" & i & return after tList
   put true into tSet["user" & i]
end repeat

put the milliseconds into tStart
repeat with i = 1 to 5000
   get ("user" & i) is among the lines of tList
end repeat
put the milliseconds - tStart into tSearch

put the milliseconds into tStart
repeat with i = 1 to 5000
   get tSet["user" & i] is true
end repeat
put the milliseconds - tStart into tKeys

put "the array is at least 3 times faster:" && (tSearch > 3 * max(1, tKeys))
Output
the array is at least 3 times faster: true

Let the engine do the loop

filter, sort, replace, split, combine, offset and the array commands (Array tools) loop inside the engine, in compiled code, much faster than a repeat written in xTalk can:

ExampleRun it in the playground
repeat with i = 1 to 50000
   put i & return after tNumbers
end repeat

put the milliseconds into tStart
repeat for each line tN in tNumbers
   if tN ends with "7" then put tN & return after tByHand
end repeat
put the milliseconds - tStart into tLoop

put the milliseconds into tStart
put tNumbers into tFiltered
filter tFiltered with "*7"
put the milliseconds - tStart into tFilter

put the number of lines of tByHand && the number of lines of tFiltered
put "filter is at least 5 times faster:" && (tLoop > 5 * max(1, tFilter))
Output
5000 5000
filter is at least 5 times faster: true

Work in variables, not in fields

A field is an object on the screen: every change to its text is laid out, and drawn when the screen next updates. Build the text in a variable, and put it into the field once:

ExampleRun it in the playground
create field "Log"

put the milliseconds into tStart
repeat 2000 times
   put "x" after field "Log"
end repeat
put the milliseconds - tStart into tField

put empty into field "Log"
put the milliseconds into tStart
repeat 2000 times
   put "x" after tText
end repeat
put tText into field "Log"
put the milliseconds - tStart into tVariable

put "the variable is at least 10 times faster:" && (tField > 10 * max(1, tVariable))
Output
the variable is at least 10 times faster: true

The same holds for any property read in a loop: the number of lines of field "Log" or the text of a field asked for on every pass. Read it once into a variable before the loop.

Changing many controls: lock screen

When a handler moves, hides or recolours many controls, the screen may be redrawn after each change. lock screen holds the drawing until the handler ends or unlock screen, so the user sees one finished change, sooner (Animation and games uses it for every frame). lock messages similarly stops openCard, setProp and other messages while a script goes through many cards or objects, for a script that only needs to look at them.

ScriptPut it in an object's script
on recolourEverything pColour
   lock screen
   repeat with i = 1 to the number of controls of this card
      set the backgroundColor of control i of this card to pColour
   end repeat
   unlock screen
end recolourEverything

on countAllFields
   local tCount
   lock messages
   repeat with i = 1 to the number of cards of this stack
      add the number of fields of card i of this stack to tCount
   end repeat
   unlock messages
   return tCount
end countAllFields

Try it yourself

This function finds the words that appear more than once in a text, but it looks each word up in a list of the words so far, and gets slow as the text grows. Rewrite it with an array of counts, check that both give the same answer for the cat saw the dog and the dog saw the cat, and put the repeated words, sorted.

ExampleRun it in the playground
function repeatedWords pText
   local tSeen, tRepeated
   repeat for each word tWord in pText
      if tWord is among the lines of tSeen and tWord is not among the lines of tRepeated then
         put tWord & return after tRepeated
      end if
      put tWord & return after tSeen
   end repeat
   sort tRepeated
   return line 1 to -1 of tRepeated
end repeatedWords

put repeatedWords("the cat saw the dog and the dog saw the cat")
Output
cat
dog
saw
the
Show a solution
ExampleRun it in the playground
function repeatedWords pText
   local tCount, tWord, tRepeated
   repeat for each word tWord in pText
      add 1 to tCount[tWord]
   end repeat
   repeat for each key tWord in tCount
      if tCount[tWord] > 1 then put tWord & return after tRepeated
   end repeat
   sort tRepeated
   return line 1 to -1 of tRepeated
end repeatedWords

put repeatedWords("the cat saw the dog and the dog saw the cat")
Output
cat
dog
saw
the

Each word is now looked up as a key, in the same short time however many words came before, so twice the text takes about twice as long, not four times.

Words in this lesson

Each one finds every lesson that uses it. The Dictionary has the whole story of each, with its syntax and more examples.