Very puzzing AppleScript performance issues with script objects

I have observed that the same piece of code, when placed inside a script object sometimes runs an order of magnitude slower than when outside any script object. Has anyone else seen this??? Here’s an example:

-- Simple handler to calculate a hash value. Outside of any script object.
on calcHash(aKey)
    set hashValue to 0
    repeat with aCharacter in aKey
        set c to ASCII number aCharacter
        set hashValue to hashValue + c
    end repeat
    return ((hashValue mod 1000) + 1)
end calcHash

-- Exact same code inside a script object.
on createTestObject()
    script
        on calcHash(aKey)
            set hashValue to 0
                repeat with aCharacter in aKey
                    set c to ASCII number aCharacter
                    set hashValue to hashValue + c
                end repeat
            return ((hashValue mod 1000) + 1)
        end calcHash
    end script
end createTestObject

-- Scenario #1: Run the handler that is outside the script object
set loopCount to 1000
repeat with i from 1 to loopCount
    calcHash("disneyland")
end repeat

-- Scenario #2: Now run the handler via a script object
set myTest to createTestObject()
repeat with i from 1 to loopCount
    tell myTest to calcHash("disneyland")
end repeat

I find that Scenario #2 takes about 10x longer to run than Scenario #1.

Anyone have any idea what could be going on here???

Cheers,
Steve

Hello!

My conclusions differ! I ran the script some times, and the same pattern emerged, even before I increased the number of iterations. 15/5000 is 0,03 and 14/5000 is 0,028 so you know how I arrived at the numbers.

The script was run on Snow Leopard using Script Debugger 4.5.7


-- Simple handler to calculate a hash value. Outside of any script object.
on calcHash(aKey)
	set hashValue to 0
	repeat with aCharacter in aKey
		set c to ASCII number aCharacter
		set hashValue to hashValue + c
	end repeat
	return ((hashValue mod 1000) + 1)
end calcHash

script sVersion
	on calcAHash(aKey)
		set hashValue to 0
		repeat with aCharacter in aKey
			set c to ASCII number aCharacter
			set hashValue to hashValue + c
		end repeat
		return ((hashValue mod 1000) + 1)
	end calcAHash
end script


-- Exact same code inside a script object.
on createTestObject()
	script
		on calcHash(aKey)
			set hashValue to 0
			repeat with aCharacter in aKey
				set c to ASCII number aCharacter
				set hashValue to hashValue + c
			end repeat
			return ((hashValue mod 1000) + 1)
		end calcHash
	end script
end createTestObject
set t1 to current date
-- Scenario #1: Run the handler that is outside the script object
set loopCount to 5000
repeat with i from 1 to loopCount
	calcHash("disneyland")
end repeat
set t2 to current date
set s1 to t2 - t1
--15,15
-- Scenario #2: Now run the handler via a script object
set t1 to current date
set myTest to createTestObject()
repeat with i from 1 to loopCount
	tell myTest to calcHash("disneyland")
end repeat
set t2 to current date
set s2 to t2 - t1
--14,14
set t1 to current date

set myTest to createTestObject()
set calch to myTest's calcHash
repeat with i from 1 to loopCount
	calch("disneyland")
end repeat
set t2 to current date
set s3 to t2 - t1
--14,14
set t1 to current date
set myTest to createTestObject()
repeat with i from 1 to loopCount
	calcHash("disneyland") of myTest
end repeat
set t2 to current date
set s4 to t2 - t1
-- 15,15
set t1 to current date
set myTest to createTestObject()
repeat with i from 1 to loopCount
	calcHash("disneyland") of my myTest
end repeat
set t2 to current date
set s5 to t2 - t1
--14,14
set t1 to current date
set myTest to createTestObject()
repeat with i from 1 to loopCount
	calcAHash("disneyland") of sVersion
end repeat
set t2 to current date
set s6 to t2 - t1
--15,15=0,03 0,03/0,028 * 100 = 1.07 ~ 7% slower.
set t1 to current date
set myTest to createTestObject()
repeat with i from 1 to loopCount
	calcAHash("disneyland") of my sVersion
end repeat
set t2 to current date
set s7 to t2 - t1

--14,14 / 5000 = 0,028

My conclusion is that as long as something is referenced like from within a script object, then it goes faster. The explanation, as I can recall it is that memory protection and/or validation is then turned off, which speeds up operations considerably. Not so in this case, only 7%

First of all, thank you for taking the time for looking into this and for expanding the test conditions. I change the loop count to 1000, and ran the tests several times. The results were consistently in this range:

(s1 = 3)
(s2 = 4)
(s3 = 3)
(s4 = 3)
(s5 = 4)
(s6 = 3)
(s7 = 71)

Notice that the 7th scenario is off the charts. This seems to differ from what you observed…correct? I wonder what could account for this difference?

Also, do you use Script Debugger? Whereas this set of tests took less than 100 seconds when run from AppleScript Editor, it took 3 minutes! or longer (I gave up after 3 minutes) when run from within Script Debugger. Is it possible that Script Debugger instruments the code so extensively?

Hi. How long has it been since you logged out of your user account? Maybe you should retest after logging out (or restarting).
Using a G4 Mac Mini, running OS 10.4.11, I achieved an average 2 second result (with both handler and object versions) on 2500 iteration runs. Tests were run in Script Editor. I made a few very minor changes”such as removing the handler wrapper from the object”but the essentials are the same:


property loopCount : 2500
set aKey to "disneyland"
global aKey

set theTime to current date
repeat loopCount times
	-----------------------------Enable only ONE statement, bounded herein:
	calcHash(aKey) --handler
	--run Object --object
	-----------------------------
end repeat
(current date) - theTime



on calcHash(aKey)
	set hashValue to 0
	repeat with aCharacter in aKey
		set c to ASCII number aCharacter
		set hashValue to hashValue + c
	end repeat
	((hashValue mod 1000) + 1)
end calcHash

script Object
	set hashValue to 0
	repeat with aCharacter in aKey
		set c to ASCII number aCharacter
		set hashValue to hashValue + c
	end repeat
	((hashValue mod 1000) + 1)
end script

Hello!

This function isn’t that typical, but I definetively learned something from this! That is, for handlers, a reference helps! (Not so with the “end of list construct” for instance).

The other thing that isn’t typical, at least for me, is that scripts are generally, if not paged totally out, not very close, when, and if I rerun a script.

What I am trying to say, that in a typical run time situation, the first time load, and performance is really interesting to measure, because this is what I perceive as speed really, the first time run of a script, and this test doesn’t do that situation justice.

So, maybe in reality, it is faster to use regular handlers, I can’t see that this test has been conclusive in any way regarding that matter. But this is very hard to test, as only the first time counts! The environment is spoiled during the next run, due to having everything in memory, and references there set up, up front. :slight_smile:

I can’t understand why this is taking so long time in AppleScript Editor, it could have been fun to see the performance of Smile as well.

I did not run the code in debug mode, but it seems like it does, doesn’t it! :slight_smile:

Your version with 2,500 iterations gives both 7 seconds on my machine with Script debugger.

1 second in Smile ans AppleScript Editor. I miss my 12 inch Quicksilver. :slight_smile:

Using Smile: after the first, chrono returns time elapsed since last call to chrono.

-- Scenario #1: Run the handler that is outside the script object
chrono
set loopCount to 1000
repeat with i from 1 to loopCount
	calcHash("disneyland")
end repeat
log chrono / loopCount
(*0.003965925791*)
-- Scenario #2: Now run the handler via a script object
set myTest to createTestObject()
log chrono
(*0.175834078691*)
repeat with i from 1 to loopCount
	
	tell myTest to calcHash("disneyland")
end repeat
log chrono / loopCount
(*0.003451762594*)

When it gets really funny is when you put “set myTest to createTestObject()” inside the loop.

If you have debugging on SD uses a different Applescript component, so the times can differ significantly. Depending on the version, displaying the event log or not can make a big difference too.

Run times using Script Debugger with debugging disabled:

(s1 = 6)
(s2 = 14)
(s3 = 27)
(s4 = 29)
(s5 = 38)
(s6 = 54)
(s7 = 71)

Exact same code running in AppleScript Editor:

(s1 = 5)
(s2 = 6)
(s3 = 5)
(s4 = 6)
(s5 = 5)
(s6 = 6)
(s7 = 71)

Conclusions: (1) Something seriously wrong with SD, perhaps leaking memory? (2) Interesting that the last test was exactly the same.

Which version of SD, and was the log window closed? FWIW, with SD5 and the event log hidden, I get values of 5, 5, 5, 5, 5, 6, 5.

I’m inclined to believe that it keeps track of memory even when not in debugging mode.

SD 5.0.3 and, yes, the event log was hidden.

Putting code in a script object doesn’t make it always faster because you can use script objects in a wrong way. Here is an example when it will improved when using an hash function. This way by calling the hash function by itself and not outside the script object keeps the lines short and therefore it will be executing a bit faster. Also I don’t use ASCII number because that’s deprecated, you could use character id instead. I’m not only executing the hash function faster but also resolving and returning it’s associated values.


set dict to hashTable's instance()'s init()

if dict's setValueForKey("aKey", "Hello World!") then
	repeat 1000 times
		dict's getValueForKey("aKey")
	end repeat
end if

script hashTable
	on instance()
		script hashTableInstance
			property parent : hashTable
			property hashTable : missing value
			property valueTable : missing value
		end script
	end instance
	
	on init()
		set my valueTable to {}
		set my hashTable to {}
		repeat 1000 times
			set end of my hashTable to {}
		end repeat
		return me
	end init
	
	on setValueForKey(_key, _value)
		set x to hashForKey(_key)
		considering case
			tell item x of my hashTable
				repeat with node in it
					if item 1 of node is equal to _key then
						set item (item 2 of node) of my valueTable to _value
						return true
					end if
				end repeat
				set end of my valueTable to _value
				set end of it to {_key, count my valueTable}
				return true
			end tell
		end considering
		return missing value
	end setValueForKey
	
	on getValueForKey(_key)
		set x to my hashForKey(_key)
		considering case
			tell item x of my hashTable
				repeat with node in it
					if item 1 of node is equal to _key then return item (item 2 of node) of my valueTable
				end repeat
			end tell
		end considering
		return missing value
	end getValueForKey
	
	on hashForKey(_key)
		set _key to _key's id
		set sum to count _key
		repeat with char in _key
			set sum to sum + char
		end repeat
		return (sum mod 1000) + 1
	end hashForKey
end script