In Skyrim, however, things are different. Scripts are no longer forced to run within a single frame, but are instead allotted certain amounts of processing time, of which there is a limited supply. What this means for scripters is that instead of scripts affecting framerate and relying on it for timing, scripts now affect the speed of other scripts and rely on them for timing. As I said before, scripting has become a zero sum game.
This means that optimisation and awareness of efficiency have become much more important than ever before. While the increase in efficiency made by making a single change to a more efficient way of doing the same thing may be miniscule, it all adds up in the end. After seeing the effects of trying to run script-heavy mods written with little or no awareness of these issues, I think it would be prudent to start a discussion on some "Best Practice" standards for Papyrus scripting.
Eventually, I want this discussion (which I hope will be ongoing) to result in a "Papyrus Best Practices" page on the Creation Kit wiki. However, the wiki isn't the best place for discussion, so I wanted to start talking about it here instead of going ahead and starting to write such standards on my own. After all, I don't think that's how they should be developed - they should be a result of community discussion and, hopefully, eventual agreement. I hope to have some constructive discussion of efficiency, optimisation, and what should and shouldn't become "Best Practice" standards before the information gets put on the wiki for all to see.
First and foremost, I think this generic "rule of thumb" should be the most important thing to consider when scripting in Skyrim:
Run as much code as necessary as often as necessary, and no more.
With the recent http://www.gamesas.com/topic/1409880-relz-wipz-code-comparer-papyrus/ of JustinOther's and my http://skyrim.nexusmods.com/mods/23782 utility, it's become much easier to compare similar snippets of code in order to determine which of them is objectively faster.
For example, it can be used to compare various methods of accessing the player as an Actor:
Game.GetForm(0x14)
Game.GetFormFromFile(0x14, "Skyrim.esm")
Game.GetPlayer()
Actor Property PlayerRef Auto ; Auto-filled in Creation Kit
By comparing these snippets, which should all be functionally identical to one another, it is possible to determine which is the best to use. In this case, the property wins by a very clear margin.
[09/03/2012 - 12:13:39AM] Code Comparer log opened (PC)[09/03/2012 - 12:13:39AM] Code Comparer Version: 1.000000[09/03/2012 - 12:13:39AM] Skyrim Version: 1.7.7.0[09/03/2012 - 12:13:39AM] SKSE Version: 1.051100[09/03/2012 - 12:13:51AM] Calibration Complete for 10^4 iterations: 0.041440 for each empty test[09/03/2012 - 12:13:51AM] === 'GetForm' ===[09/03/2012 - 12:13:51AM] Started 'GetForm' at: 34.956001 | Iterations to complete: 10000[09/03/2012 - 12:13:51AM] Finished 'GetForm' at: 35.067562 | Iterations completed: 10000[09/03/2012 - 12:13:51AM] Time elapsed (Raw) for 'GetForm': 0.153000[09/03/2012 - 12:13:51AM] Time elapsed (Calibrated) for 'GetForm': 0.111560[09/03/2012 - 12:13:51AM] Approximate time for each iteration (Raw): 0.000015[09/03/2012 - 12:13:51AM] Approximate time for each iteration (Calibrated): 0.000011[09/03/2012 - 12:14:01AM] Calibration Complete for 10^4 iterations: 0.041820 for each empty test[09/03/2012 - 12:14:01AM] === 'GetFormFromFile' ===[09/03/2012 - 12:14:01AM] Started 'GetFormFromFile' at: 45.202000 | Iterations to complete: 10000[09/03/2012 - 12:14:01AM] Finished 'GetFormFromFile' at: 45.315178 | Iterations completed: 10000[09/03/2012 - 12:14:01AM] Time elapsed (Raw) for 'GetFormFromFile': 0.154999[09/03/2012 - 12:14:01AM] Time elapsed (Calibrated) for 'GetFormFromFile': 0.113179[09/03/2012 - 12:14:01AM] Approximate time for each iteration (Raw): 0.000015[09/03/2012 - 12:14:01AM] Approximate time for each iteration (Calibrated): 0.000011[09/03/2012 - 12:14:15AM] Calibration Complete for 10^4 iterations: 0.041470 for each empty test[09/03/2012 - 12:15:57AM] === 'getplayer' ===[09/03/2012 - 12:15:57AM] Started 'getplayer' at: 59.305000 | Iterations to complete: 10000[09/03/2012 - 12:15:57AM] Finished 'getplayer' at: 161.638519 | Iterations completed: 10000[09/03/2012 - 12:15:57AM] Time elapsed (Raw) for 'getplayer': 102.374992[09/03/2012 - 12:15:57AM] Time elapsed (Calibrated) for 'getplayer': 102.333519[09/03/2012 - 12:15:57AM] Approximate time for each iteration (Raw): 0.010237[09/03/2012 - 12:15:57AM] Approximate time for each iteration (Calibrated): 0.010233[09/03/2012 - 12:16:07AM] Calibration Complete for 10^4 iterations: 0.041450 for each empty test[09/03/2012 - 12:16:08AM] === 'PlayerRef' ===[09/03/2012 - 12:16:08AM] Started 'PlayerRef' at: 171.867996 | Iterations to complete: 10000[09/03/2012 - 12:16:08AM] Finished 'PlayerRef' at: 171.868561 | Iterations completed: 10000[09/03/2012 - 12:16:08AM] Time elapsed (Raw) for 'PlayerRef': 0.042007[09/03/2012 - 12:16:08AM] Time elapsed (Calibrated) for 'PlayerRef': 0.000558[09/03/2012 - 12:16:08AM] Approximate time for each iteration (Raw): 0.000004[09/03/2012 - 12:16:08AM] Approximate time for each iteration (Calibrated): 0.000000[09/03/2012 - 12:16:19AM] Log closed
From this result, I would say that it should be considered best practice to access the player object via an Actor property that has been assigned a value in the Creation Kit. Whereas directly pointing object properties at specific objects via the Creation Kit does cause them to remain permanently persistent, this is not a concern when applied to the player.
Another, less important general rule that I try to apply where possible is this:
All else being equal, a native solution is better.
Doing something manually via Papyrus that can be done natively is going to both be slower and have a bigger effect on the Papyrus engine and therefore other scripts. If it's possible to implement a native solution, it's generally the better option.
There are caveats, though, hence the "All else being equal..." part of that rule. Take, for example, the Math.floor() function. It's a native global function, accessible via the Math script, that truncates a floating point value and returns it as an integer. The following two code snippets are functionally equivalent, but which should be used?
Int Foo = 3.14159265359 as Int
Int Foo = TestLib.QuickFloor(3.14159265359); Using this in TestLib.pscInt Function QuickFloor(Float afValue) Global Return afValue as IntEndFunction
Int Foo = Math.Floor(3.14159265359)
[08/29/2012 - 07:44:54PM] Code Comparer log opened (PC)[08/29/2012 - 07:44:54PM] Skyrim Version: 1.7.7.0[08/29/2012 - 07:44:54PM] SKSE Version: 1.051100[08/29/2012 - 07:44:54PM] =========================[08/29/2012 - 07:44:59PM] Int Foo = 3.14159265359 as Int[08/29/2012 - 07:44:59PM] Started test 0 at: 34.506001 | Iterations to complete: 10000[08/29/2012 - 07:44:59PM] Finished test 0 at: 34.596001 | Iterations completed: 10000[08/29/2012 - 07:44:59PM] Time elapsed for test 0: 0.090000[08/29/2012 - 07:44:59PM] Approximate time for each iteration: 0.000009[08/29/2012 - 07:44:59PM] =========================[08/29/2012 - 07:45:01PM] Int Foo = TestLib.QuickFloor(3.14159265359)[08/29/2012 - 07:45:01PM] Started test 1 at: 36.476002 | Iterations to complete: 10000[08/29/2012 - 07:45:01PM] Finished test 1 at: 37.084999 | Iterations completed: 10000[08/29/2012 - 07:45:01PM] Time elapsed for test 1: 0.608997[08/29/2012 - 07:45:01PM] Approximate time for each iteration: 0.000061[08/29/2012 - 07:45:01PM] =========================[08/29/2012 - 07:45:04PM] Int Foo = Math.Floor(3.14159265359)[08/29/2012 - 07:45:04PM] Started test 2 at: 40.035000 | Iterations to complete: 10000[08/29/2012 - 07:45:04PM] Finished test 2 at: 40.174999 | Iterations completed: 10000[08/29/2012 - 07:45:04PM] Time elapsed for test 2: 0.139999[08/29/2012 - 07:45:04PM] Approximate time for each iteration: 0.000014[08/29/2012 - 07:45:04PM] =========================[08/29/2012 - 07:45:11PM] Log closed
In this case, Math.floor() is more efficient than casting a Float to an Int, but only with all else being equal. In this case, that means when a global function is called as part of the code. However, since Math.floor() requires calling a global function but casting a Float to an Int doesn't, the inline casting method is actually more efficient and, I'd argue, should be considered best practice.
In fact, comparing global functions, local functions, and inline code, you can see that there's generally an even greater overhead associated with calling local functions. Here are some result from the code comparer comparing a global function, local function, and inline code snippet all essentially using the following code:
Float Foo = 3.14; ...Foo as IntThe Math.Floor() native global function was added as an extra test too. The profiling logs showed no queueing, so these results should be able to be taken as-is:
[09/02/2012 - 04:20:59PM] Code Comparer log opened (PC)[09/02/2012 - 04:20:59PM] Skyrim Version: 1.7.7.0[09/02/2012 - 04:20:59PM] SKSE Version: 1.051100[09/02/2012 - 04:21:12PM] Calibration Complete: 0.022330 for 10000 iterations.[09/02/2012 - 04:21:12PM] === 'Math's Floor' ===[09/02/2012 - 04:21:12PM] Started 'Math's Floor' at: 36.118999 | Iterations to complete: 10000[09/02/2012 - 04:21:12PM] Finished 'Math's Floor' at: 36.224670 | Iterations completed: 10000[09/02/2012 - 04:21:12PM] Time elapsed for 'Math's Floor': 0.105672[09/02/2012 - 04:21:12PM] Approximate time for each iteration: 0.000011[09/02/2012 - 04:21:12PM] === 'As Int' ===[09/02/2012 - 04:21:12PM] Started 'As Int' at: 36.263000 | Iterations to complete: 10000[09/02/2012 - 04:21:12PM] Finished 'As Int' at: 36.269669 | Iterations completed: 10000[09/02/2012 - 04:21:12PM] Time elapsed for 'As Int': 0.006670[09/02/2012 - 04:21:12PM] Approximate time for each iteration: 0.000001[09/02/2012 - 04:21:12PM] === 'GlobalFloor' ===[09/02/2012 - 04:21:12PM] Started 'GlobalFloor' at: 36.304001 | Iterations to complete: 10000[09/02/2012 - 04:21:12PM] Finished 'GlobalFloor' at: 36.442669 | Iterations completed: 10000[09/02/2012 - 04:21:12PM] Time elapsed for 'GlobalFloor': 0.138669[09/02/2012 - 04:21:12PM] Approximate time for each iteration: 0.000014[09/02/2012 - 04:21:13PM] === 'LocalFloor' ===[09/02/2012 - 04:21:13PM] Started 'LocalFloor' at: 36.490002 | Iterations to complete: 10000[09/02/2012 - 04:21:13PM] Finished 'LocalFloor' at: 36.769669 | Iterations completed: 10000[09/02/2012 - 04:21:13PM] Time elapsed for 'LocalFloor': 0.279668[09/02/2012 - 04:21:13PM] Approximate time for each iteration: 0.000028[09/02/2012 - 04:21:33PM] Log closedAnother test done in reverse order:
[09/02/2012 - 04:33:25PM] Code Comparer log opened (PC)[09/02/2012 - 04:33:25PM] Skyrim Version: 1.7.7.0[09/02/2012 - 04:33:25PM] SKSE Version: 1.051100[09/02/2012 - 04:33:39PM] Calibration Complete: 0.041860 for 10000 iterations.[09/02/2012 - 04:33:39PM] === 'LocalFloor' ===[09/02/2012 - 04:33:39PM] Started 'LocalFloor' at: 42.231998 | Iterations to complete: 10000[09/02/2012 - 04:33:39PM] Finished 'LocalFloor' at: 42.489140 | Iterations completed: 10000[09/02/2012 - 04:33:39PM] Time elapsed for 'LocalFloor': 0.257140[09/02/2012 - 04:33:39PM] Approximate time for each iteration: 0.000026[09/02/2012 - 04:33:39PM] === 'GlobalFloor' ===[09/02/2012 - 04:33:39PM] Started 'GlobalFloor' at: 42.592999 | Iterations to complete: 10000[09/02/2012 - 04:33:39PM] Finished 'GlobalFloor' at: 42.706142 | Iterations completed: 10000[09/02/2012 - 04:33:39PM] Time elapsed for 'GlobalFloor': 0.113143[09/02/2012 - 04:33:39PM] Approximate time for each iteration: 0.000011[09/02/2012 - 04:33:39PM] === 'As Int' ===[09/02/2012 - 04:33:39PM] Started 'As Int' at: 42.789001 | Iterations to complete: 10000[09/02/2012 - 04:33:39PM] Finished 'As Int' at: 42.799141 | Iterations completed: 10000[09/02/2012 - 04:33:39PM] Time elapsed for 'As Int': 0.010138[09/02/2012 - 04:33:39PM] Approximate time for each iteration: 0.000001[09/02/2012 - 04:33:39PM] === 'Math's Floor' ===[09/02/2012 - 04:33:39PM] Started 'Math's Floor' at: 42.862000 | Iterations to complete: 10000[09/02/2012 - 04:33:39PM] Finished 'Math's Floor' at: 42.944141 | Iterations completed: 10000[09/02/2012 - 04:33:39PM] Time elapsed for 'Math's Floor': 0.082141[09/02/2012 - 04:33:39PM] Approximate time for each iteration: 0.000008[09/02/2012 - 04:33:56PM] Log closed
Both of these results rank the different methods in the same order:
- Inline code
- Native global function
- Global function
- Local function
As a result of these tests, I'd also like to propose this as a best practice:
All functions that could be global* should be global, and all others should not.
* i.e. all functions that don't need to know anything about the object to which their script is attached
The reason why I'm not recommending that all functions should be global (after all, they do have less overhead) is that this would mean they're not thread safe. See http://www.creationkit.com/Threading_Notes_(Papyrus) for more information.
Just in case anyone starts to wonder why I've started a thread and then not replied to anything in it for quite a while, I'm out of town for the weekend and will have very little internet access. I'm actually posting this via my phone, and basically I likely won't be able to take part in any discussion myself for a couple of days.
Cipscis
EDIT 10th September 2012:
Values that will not change should only be requested once.
Values such as the player and the base object of a scripted ObjectReference will not change, so it is safe to only request them once. Repeatedly calling functions such as http://www.creationkit.com/GetPlayer_-_Game or http://www.creationkit.com/GetBaseObject_-_ObjectReference is therefore unnecessary and, what's more, will result in a slower, less efficient script.
For values that will be the same in any one instance of a script but may vary between instances, such as the http://www.creationkit.com/GetBaseObject_-_ObjectReference of a scripted ObjectReference, the best approach is probably to store its value in a variable or auto property that is set within an http://www.creationkit.com/OnInit event. It would be possibly to use auto properties that have their values set via the Creation Kit, but such cases couldn't make use of its "Auto-Fill Properties" feature and would therefore require that properties be manually assigned each time the script is attached to an object, so this is unlikely to be a more useful approach most of the time.
For values that will be the same for any instance of a script, such as the player, it is usually best to set them in the Creation Kit. Auto properties that are given the editorID of the object to which they point as their name can make use of the Creation Kit's "Auto-Fill Properties" feature. It's worth noting that this feature works for auto properties with the name "PlayerRef".
Important Note:
Pointing a property at a particular object in the Creation Kit has drawbacks when that object is an ObjectReference (including Actors), as it will force them to remain always http://www.creationkit.com/Persistence_(Papyrus). This is usually not desired, so be careful not to use the Creation Kit to point properties at ObjectReferences or Actors that shouldn't always remain persistent, and once you're done with an ObjectReference or Actor any variables or properties that were pointing at it should be set to None so that it is not forced to remain persistent.
Cipscis
Edit 11th September 2012
When registering for events, wherever possible only register for a single event.
Many registration events come in two flavours - one registers for continual events and another registers for just a single event. To use the most common example, http://www.creationkit.com/RegisterForUpdate_-_Form registers for continual http://www.creationkit.com/OnUpdate_-_Form events, whereas http://www.creationkit.com/RegisterForSingleUpdate_-_Form registers for a single OnUpdate event.
It's important to use the "register for a single event" flavour wherever possible for two reasons:
- When a mod is uninstalled, scripts that have been previously added to objects will not be removed, but the file containing the compiled script itself will likely have be removed with the rest of the mod. When this happens, the object stays registered to receive all events for which it was already registered, and the game will continue to send these events to the object even though the code that handled the events no longer exists.
Continuing to receive these events will put extra stress on the scripting system, as well as filling up the Papyrus error log with a new error every time the registered event is passed to the removed script. - When registering to continually receive events, especially OnUpdate events, it's possible that the next event will be sent to the script before the previous one has finished. If this happens, it will cause extra stress on the scripting system and will also cause save bloat (i.e. an increase in file size for saved games) as more events are queued on the stack and saved with the game.
The save bloat behaviour caused by registering for continual updates in this way was, to my knowledge, first diagnosed http://www.gamesas.com/topic/1349327-scripting-savegame-bloating/
Float Property UpdateInterval = 5.0 AutoEvent OnInit RegisterForSingleUpdate(UpdateInterval)EndEventEvent OnUpdate ; Do stuff RegisterForSingleUpdate(UpdateInterval)EndEvent
Always use the correct type of literal.
When passing http://www.creationkit.com/Literals_Reference, it is always best to use the correct type. Some types can automatically be http://www.creationkit.com/Cast_Reference to other types, but this should never be possible when dealing with literals.
For example, when passing arguments to functions, the correct type should always be used. This means that if a function argument is of type float, a literal of type int should not be passed. For example:
RegisterForSingleUpdate(1) ; Is less efficient than...RegisterForSingleUpdate(1.0)The reason for this difference is that, in addition to the function call that is happening regardless, a cast operation is also required that takes slightly more time and is wholly unnecessary in cases like this.
To pass a whole number as a float just add ".0" at the end. For example, 1 is an int but 1.0 is a float.
Because all types can be automatically cast to bool, code like this is still valid:
Enable(1)However, because http://www.creationkit.com/Enable_-_ObjectReference takes a single parameter of type bool, this is a more efficient way of doing exactly the same thing:
Enable(true)
When passing non-literal values, such as those stored in variables, it is entirely alright to rely on auto-casting. In cases like that, a cast will happen whether you do it manually (e.g. "MyActorVar as bool") or you allow the compiler to handle it.
Cipscis