Performance (Papyrus)

From the Fallout4 CreationKit Wiki
Jump to navigation Jump to search

Overview[edit | edit source]

Papyrus, like other scripting and programming languages, has various ways to accomplish the same task. Also like these other languages, some of these methods are more preferable then others in regards to speed, memory usage, or sometimes both. Unlike most other languages, Papyrus has some special architectural choices that make performance sometimes non-intuitive.

This page is set up to provide some basic guidelines on how to improve your script's performance, memory usage, or both, as well as information on profiling your script to see what parts are slow and could use improvement.

Why should I care?[edit | edit source]

While expensive scripts won't impact your framerate, they will impact each other's performance. The more scripts that are running at the same time, the slower each of them is going to run, in general. Therefore, for the best responsiveness, it is preferred to do your work as quickly as possible and exit.

In terms of persistence, having fewer objects persistent means fewer objects have to stay loaded. The fewer objects that are loaded, the better your framerate will be, and the less memory the game will use. And a better framerate equal better script performance as well.

Scripts are cheap, right?[edit | edit source]

Profiling data from a single function call that called functions on two other objects, one of which was hotly contested. Note the long delays on functions called on OBJ B as they have to wait for other scripts to finish.

<OBJ B - Function 1> - 1790.75ms
   - 0.06ms
  <queue push> - 1454.16ms
  <OBJ B - Function 1> - 0.02ms
     - 0.02ms
   - 1.47ms
  <OBJ B - Function 2> - 0.07ms
     - 0.05ms
  <OBJ C - Function 1> - 0.07ms
     - 0.07ms
   - 0.16ms
  <queue push> - 333.89ms
  <OBJ B - Function 3> - 0.42ms
...

A second set of profiling data from a single function call, this time calling another function 3 times in row. This second function would repeat the same work on the same object, rather then having the data passed in via a parameter.

<Function A> - 7233.80ms
   - 0.08ms
  <Function B> - 5937.31ms
     - 0.03ms
    ObjectReference..X - 14.29ms
       - 0.02ms
      <queue push> - 8.76ms
      ObjectReference..GetPositionX - 0.02ms
         - 0.02ms
       - 5.49ms
     - 0.06ms
    ObjectReference..Y - 5762.33ms
       - 0.04ms
      <queue push> - 5760.60ms
      ObjectReference..GetPositionY - 0.04ms
         - 0.04ms
       - 1.66ms
     - 0.15ms
    <queue push> - 121.20ms
    ObjectReference..Z - 33.04ms
       - 0.76ms
      <queue push> - 20.75ms
      ObjectReference..GetPositionZ - 0.02ms
         - 0.02ms
       - 5.45ms
      <queue pop> - 6.06ms
     - 6.20ms
    math..sqrt - 0.01ms
       - 0.01ms
     - 0.02ms
   - 0.06ms
  <Function B> - 161.81ms
    Getting X, Y, and Z from the same object, getting the same values, repeating work...
  <Function B> - 649.49ms
    Getting X, Y, and Z from the same object, getting the same values, repeating work...
...

Speed Improvements[edit | edit source]

Don't re-do work[edit | edit source]

Bad:

if getLinkedRef(myKeyword)
  if !someStatus
    ; Do stuff here
    if getDistance(getLinkedRef(myKeyword)) > 64
      moveto(getLinkedRef(myKeyword))
    endif
  else
    ; Do other stuff here
  endif
endif

Better:

ObjectReference linkedRef = getLinkedRef(myKeyword)
if linkedRef
  if !someStatus
    ; Do stuff here
    if getDistance(linkedRef) > 64
      moveto(linkedRef)
    endif
  else
    ; Do other stuff here
  endif
endif

If you perform an operation and the result of said operation isn't going to change, don't keep doing it over and over again. Store the result in a variable and re-use the variable where needed. Prefer to store the variable in a function instead of your script to reduce memory costs and reduce persistence.

Don't handle events you don't have to[edit | edit source]

Bad:

EVENT onLoad()
  RegisterForHitEvent(self)
endEVENT

EVENT onHit(...)
  if PropertyValue == TRUE
    DoWork()
  endif
endEVENT

Better:

EVENT onLoad()
  if PropertyValue == TRUE
    RegisterForHitEvent(self)
  endif
endEVENT

EVENT onHit(...)
  DoWork()
endEVENT

If you are not interested in an event, then don't request it, or don't handle it. Handling an event and checking a condition is slower then checking the condition before asking for the event, or using a script state to route the event to an empty handler.

Fail fast, fail early[edit | edit source]

Bad:

if Function() && OtherFunction() == false
  Actor meActor = (self as ObjectReference) as Actor
  if meActor
    meActor.SetUnconscious(true)
  endif
endif

Better:

Actor meActor = (self as ObjectReference) as Actor
if meActor && Function() && OtherFunction() == false
  meActor.SetUnconscious(true)
endif

Certain operations, like casting, are faster then others, like function calls. If you test things that are fast early, then if they fail you'll have saved time doing the more expensive checks. Also, if a particular check fails more often then the others, prefer to put it first. In the above example, almost everything passed to the function wasn't an actor, so making the actor check first eliminated several calls to the two functions.

Know your language features and functions[edit | edit source]

Bad:

ObjectReference nextRef = GetLinkedRef()
while nextRef
  ; do something with nextRef
  nextRef = nextRef.GetLinkedRef()
endWhile

Better:

ObjectReference[] refChain = GetLinkedRefChain()
int curIndex = 0
while curIndex < refChain.Length
  ObjectReference curRef = refChain[curIndex]
  ; do something with curRef
  curIndex += 1
endWhile

There are some functions in Papyrus that will do what you're trying to do natively, so you don't have to spend time doing it yourself. In the above case, it's faster to call a single function to get an array of all linked refs, and iterate over that array, then to repeatedly call getLinkedRef over and over again. This mostly comes down to knowing what tools are available and where they should be used. Of course, if you don't need all the linked refs, and only want one of them, then don't get them all.

Know your language features and functions (part 2)[edit | edit source]

Bad:

Event OnActivate(...)
  if DoStuff
    ; Do a bunch of things
  endIf
EndEvent

Better:

State DoStuff
  Event OnActivate(...)
    ; Do a bunch of things
  EndEvent
EndState

Event OnActivate(...)
  ; Empty!
EndEvent

Similar to the previous point, be aware of what language features are available. In this case, using a state and an empty OnActivate event saves time, because Papyrus can detect empty functions and events and avoid calling them at all. So if you aren't in the "DoStuff" state, no code has to be run. The downside to this approach, of course, is you can only have one script state at a time, so it may not fit all usage cases.

Prefer single point of entry over multiple[edit | edit source]

Bad:

Event OnActivate(...)
  ObjectB.FunctionA()
  ObjectB.FunctionB()
  ObjectB.FunctionC()
EndEvent

Better:

Scriptname ScriptA extends ObjectReference

ScriptB ObjectB

Event OnActivate(...)
  ObjectB.DoAllTheStuff()
EndEvent

;------- Separate file -------

Scriptname ScriptB extends ObjectReference

Function DoAllTheStuff()
  FunctionA()
  FunctionB()
  FunctionC()
EndFunction

Because every time you call a function on another script that isn't you or a script you extend (i.e. the "self" variable would be different - even if it's attached to the same form) there is a cost in switching. This cost may be expensive if the object is used by a lot of scripts (i.e. the player). So if you find yourself calling multiple functions in a row on the same object, it might be better to group them into a function on the target object and call that instead. That way the switch cost is only paid once. This may not always be possible, however, especially if the destination script isn't one you own.

Additional Notes[edit | edit source]

Calling a native function usually (but not always) syncs to the framerate of the game. So if you call 30 native functions and the game is running at 30 fps, it will take (roughly) 1 second to get through them all, assuming no other extenuating circumstances. The exception are non-delayed functions, which are dispatched immediately. (Note that some of these may be latent and so may take some time to return, even if they immediately dispatch)

Also, if someone else is busy working on an object, everyone else will have to wait their turn. For example, if 10 scripts all want to ask the player what level they are, they all have to take a number and go in order. As such, if you find yourself making "controller" scripts, you may want to carefully monitor how much they are used to make sure you're not slowing yourself down by talking to them all the time. Note that if you call a function outside your script, or a latent function, the next person waiting to talk to your object will get a chance to run.

Memory and Persistence[edit | edit source]

Prefer events[edit | edit source]

Bad:

While HasDirectLOS(OtherObj)
  DoStuff()
  Utility.Wait(1)
EndWhile

Better:

bool doingStuff

Function SomeFunction()
  doingStuff = true
  RegisterForDirectLOSLost(self, OtherObj)
  StartTimer(1, DoStuffID)
EndFunction

Event OnLostLOS(...)
  doingStuff = false
  CancelTimer(DoStuffID)
EndEvent

Event OnTimer(int ID)
  if ID == DoStuffID
    DoStuff()
    if doingStuff
      StartTimer(1, DoStuffID)
    endIf
  endIf
EndEvent

Loops polling for things, or doing things over and over again, consume resources while running. Both memory and processor time. If possible, prefer to use events instead of polling, and repeated timers instead of long-running loops (and make sure you have a way out of the timer loop!). Waiting for events is much easier on the system then constantly asking "are you done yet?"

Prefer function variables over script variables[edit | edit source]

Bad:

ObjectReference linkedRef

Event OnInit()
  linkedRef = GetLinkedRef()
EndEvent

Event OnActivate(...)
  ; Do stuff with linkedRef
EndEvent

Better:

Event OnActivate(...)
  ObjectReference linkedRef = GetLinkedRef()
  ; Do stuff with linkedRef
EndEvent

While the first example may be better for performance - you aren't getting the linked ref every single time you are activated - it is worse on memory and persistence. This is because once you've stuck an object into a variable, that object is now persistent until that variable goes away. If that variable is a member of a script, this may be a very long time (or even never).

Prefer to use a function-local variable instead, even though it will cost you a little time in the function. This is because function-local variables go away at the end of the function, releasing the persistence hold on the object they contain.

If you must make an script variable to hold something, then try to clear it by setting it to None when you no longer need it.

Don't register for events that can't happen[edit | edit source]

Bad:

Event OnInit()
  RegisterForHitEvent(self)
EndEvent

Better:

Event OnLoad()
  RegisterForHitEvent(self)
EndEvent

Don't register for an event if the event can't realistically occur at the time of registration. In the above example, it doesn't make sense to register for a weapon hit in OnInit, because this object may not even have 3d to be hit. It's better to wait til the object's 3d is loaded first, before asking for hit events.

Circular References[edit | edit source]

Bad:

Scriptname ScriptA extends ObjectReference

ObjectReference ThingIMade

Event OnActivate(...)
  ThingIMade = PlaceAtMe(Thingy)
  (ThinkIMade as ScriptB).SayHello(self)
EndEvent

;------- In another file -------

Scriptname ScriptB extends ObjectReference

ObjectReference MyParent

Function SayHello(ObjectReference aParent)
  MyParent = aParent
EndFunction

... where's the better example? Well that's kind of hard to show, as it will depend on what you're doing. The problem with the above example is that ScriptA has put and object into a variable, persisting it, and then ScriptB put the original object into a variable as well, persisting it! This means neither A nor B will ever go away as long as they are in the variables. Sometimes you can't avoid this, but in general, try not to make "reference loops" like this.

Function Size[edit | edit source]

Very large functions consume more memory then small functions. This is usually due to the variety of variables (visible and hidden) in the function. As such, prefer to break up large functions into a series of calls into smaller functions. This way the original function only needs enough memory to make the calls into the smaller functions, and the smaller functions only need enough memory for themselves - giving it all back when they exit.

Not only that, but grouping your script into small, easy to understand functions will make things easier to read and follow.

When in doubt - a function should probably only do one thing, but do it well. If you find yourself doing multiple things in a function, it might be a good candidate to break into smaller ones, even if the smaller ones are only called once.

Profiling Your Scripts[edit | edit source]

Why should I profile?[edit | edit source]

People are incredibly bad and picking out what part of their script or code is actually slow. This isn't unique to scripters or modders, even seasoned programmers don't trust themselves to figure out what part of their code is slowest. So instead of guessing, we measure. In other words, we profile to find out what part of the code or script takes the longest and why, so we can focus our efforts on the parts where we'll get the most bang for our buck.

Setup[edit | edit source]

Before any of the Papyrus profiling console commands and functions will do anything, you need to edit your ini file to turn profiling on. This is off by default because profiling can slow the script system down slightly. To turn it on, set the "bEnableProfiling" option in the "[Papyrus]" section of your custom ini to 1.

[Papyrus]
bEnableProfiling=1

How to profile[edit | edit source]

There are a few ways to profile, depending on what kind of information you want.

The most wide-ranging option is to turn on global profiling using the TPGP console command. This will log everything the script system does from when it is turned on til when it is turned off. This may be too much information, but if you're tracking down why you have general "script lag" it can at least give you a good starting point to do finer-grained profiling by pointing out hot spots.

The next most common option is to use the StartStackProfiling script command in a particular event or command you want to measure. This will log everything that a particular thread of execution does, once the start command is called.

There are also other, more specialized, commands which vary in usefulness. Most of them are either in the debug script or listed on the papyrus console commands page. The one thing to keep in mind is that a lot of these more specific commands are filtered, so if you say, profile a script, you'll know how long a function takes in that script, but any calls into things that aren't that script won't be measured, so it might be hard to tell if a set stage is taking too long if you're measuring your quest script (for example).

Now what?[edit | edit source]

After profiling, you will end up with logs in your Script folder in the Profiling sub-folder. This will generally be "<User Documents Folder>\My Games\Fallout4\Logs\Script\Profiling".

You can then open these logs using the profile log tool in the Tools folder where your CK was installed.

The tool will then give you a screen listing every function that was recorded, and various statistics for that particular function. If you select one of the functions then the right panel will show you the complete call tree of a particular thread that executed that function (you can change which call you view with the dropdown). Using that timing information, you should be able to figure out what is making your script slow, and hopefully figure out how to resolve it.

I can't remember all this![edit | edit source]

Well, you can always come back here for a review, but also try to get coworkers or friends to look at your scripts. They may be able to spot things you miss, or give you alternate solutions to your issues. They may even give you an idea for a new feature, or how to make your script more reusable.