Formula optimization: speed up your formula eval code!

Tsukihime

Veteran
Veteran
Joined
Jun 30, 2012
Messages
8,564
Reaction score
3,846
First Language
English
Killozapit posted a little snippet here.

http://www.rpgmakervxace.net/topic/26685-a-mysterious-optimization-in-yanfly-engine-ace-extra-param-formulas/

I've never thought to use procs like this, but then it became pretty obvious once I saw it.

Basically, the general pattern I use for formula evaluation is this

def eval_my_formula(formula, v=$game_variables, s=$game_switches, ... ) eval(formula)endAnd it works fine. You pass in

Code:
eval_my_formula("v[2] + 3")
And it will return whatever's in variable 2 plus 3.If var2 is 4, then the formula will return 7.

If var2 is 20, then the formula will return 23.

But eval's are expensive. It doesn't matter as much if it's a one-off thing cause it's still pretty fast for most simple formulas, but there are scripts that are constantly eval'ing conditions every other frame and that will be a huge performance hit.

So what if we don't have to do it?

The trick that killozapit shows is to store the formula itself as a lambda. You would need to eval it initially, which is expensive, but after the proc has been created, you just need to re-use it again and again.

So given my simple formula eval method, we can optimize it as follows

def eval_my_formula(formula, v=$game_variables, s=$game_switches, ... ) @myFormulaProc ||= eval("lambda { #{formula} }" ) @myFormulaProc.callendAnd now instead of eval'ing, it just needs to make a method call.After all, the purpose of eval'ing in the first place is to allow users to specify their own formulas without having to go into the code themselves and add it. The formula is essentially a proc, so why not treat it like one?

There are some problems with this implementation at this point, and the biggest one you need to avoid is storing the proc with an instance of any object that you will eventually marshal, cause that will just fail.

Are there any other issues?
 
Last edited by a moderator:

Napoleon

Veteran
Veteran
Joined
Dec 29, 2012
Messages
869
Reaction score
97
First Language
Dutch
Primarily Uses
Good info thanks.
 

Tsukihime

Veteran
Veteran
Joined
Jun 30, 2012
Messages
8,564
Reaction score
3,846
First Language
English
Some simple benchmarking. The times are meant to demonstrate the relative improvements over a million runs. The numbers are not too important since they do not show anything particularly significant.

require 'benchmark'$game = [1,2,3]Runs = 1000000Formula = "$game[0] + 3"def normal_eval eval(Formula)enddef proc_eval @proc ||= eval("lambda { #{Formula} }") @proc.callendBenchmark.bm {|x| x.report("Normal") {Runs.times { normal_eval }} x.report("Proc") { Runs.times { proc_eval } }}My reports resulted

Code:
       user     system      total        realNormal  3.985000   0.031000   4.016000 (  4.033830)Proc  0.218000   0.000000   0.218000 (  0.214169)
So given a million runs, we can see that my original way of evaluating formulas takes 20x as much time as it did to just store a proc in memory and re-use it.The real question is, does this make a real difference in practice?
 
Last edited by a moderator:

TheoAllen

Self-proclaimed jack of all trades
Veteran
Joined
Mar 16, 2012
Messages
5,592
Reaction score
6,522
First Language
Indonesian
Primarily Uses
RMVXA
Interesting concept. I never thought to use proc like this. However...

The real question is, does this make a real difference in practice?
I guess not. I believe the run time of evaling a formula is relative faster than graphics related process like loading bitmap. Since it only runs once you dealing damage to the enemy. The lag issues of battle related script often occurs in either animation (the loading bitmap process) or GUI itself (if it's badly designed).
 

Tsukihime

Veteran
Veteran
Joined
Jun 30, 2012
Messages
8,564
Reaction score
3,846
First Language
English
Interesting concept. I never thought to use proc like this. However...


I guess not. I believe the run time of evaling a formula is relative faster than graphics related process like loading bitmap. Since it only runs once you dealing damage to the enemy. The lag issues of battle related script often occurs in either animation (the loading bitmap process) or GUI itself (if it's badly designed).
If you had some conditions that were being checked every frame or so (eg: custom state removal conditions), you don't want to have that lag add on to the already laggy code.
 

Another Fen

Veteran
Veteran
Joined
Jan 23, 2013
Messages
564
Reaction score
275
First Language
German
Primarily Uses
Interesting idea. However, I also doubt it will make much of a difference in most cases.

But one more issue you should care about:
Procs keep context information like local variables when created and refer to it when called, even when no other object has access to the variables any longer.

For your example code this means the proc will grab the parameter variables from the first call and refer to them every time instead of the actual parameters. For example, v will always refer to the same instance of Game_Variables, even when the content of $game_variables has changed:

Code:
def proc_eval(a)  @proc ||= eval("Proc.new { p a }")  @proc.callend1.upto(5) { |i| proc_eval(i) }# =># 1# 1# 1# 1# 1
 
Last edited by a moderator:

Tsukihime

Veteran
Veteran
Joined
Jun 30, 2012
Messages
8,564
Reaction score
3,846
First Language
English
I read about local variables being bound to it, but didn't think much about it.

That becomes problematic. Thanks for pointing it out!

In that case, I would pass the variables on to the proc itself:

def proc_eval(a) @proc ||= eval("lambda {|a| p a }") @proc.call(a)end1.upto(5) { |i| proc_eval(i) }# =># 1# 2# 3# 4# 5And this should take care of it...putting it through the simple benchmark, it's still doing a lot better than just calling eval directly.The main advantage is being able to avoid the eval calls and being able to dynamically define methods, while not having to write too much cryptic code, so if we can iron out the issues that come with poor implementation like what I did initially, it should be a usable pattern in RM!

However, yes, the only foreseeable use cases would be for formula evaluations that are being performed in real-time (eg: every frame) for the purpose of correctness. We might choose to do it every other frame, or every second, but with all the other things going on in RM (graphics, input, parallel process events, other code, etc.), having a parallel check isn't going to help you with performance.
 
Last edited by a moderator:

TheoAllen

Self-proclaimed jack of all trades
Veteran
Joined
Mar 16, 2012
Messages
5,592
Reaction score
6,522
First Language
Indonesian
Primarily Uses
RMVXA
Question. Will it make real difference when I make this?
As Another Fen said that Procs keep the contents as the local variable.
So I'm afraid if it will generate some issues.

def proc_eval(f) @proc ||= eval("lambda { #{f} }") @proc.callendSo I made this

def proc_eval(f) @proc ||= eval("lambda {|form| eval form }") @proc.call(f)endThere is eval inside eval
 

Tsukihime

Veteran
Veteran
Joined
Jun 30, 2012
Messages
8,564
Reaction score
3,846
First Language
English
What would the purpose of the eval inside the proc be?
 

TheoAllen

Self-proclaimed jack of all trades
Veteran
Joined
Mar 16, 2012
Messages
5,592
Reaction score
6,522
First Language
Indonesian
Primarily Uses
RMVXA
My concern is like Another Fen post.

Hmm... wait. Let me try this out....

EDIT :

Tested. here is the result

Code:
def proc_eval(f)  @proc ||= eval("lambda { #{f} }")  @proc.callend ["puts 1+2", "puts 3+4", "puts 5+6"].each do |f|  proc_eval(f)end # Output :# 3# 3# 3
While this thing
Code:
def proc_eval(f)  @proc ||= eval("lambda { |form| eval form }")  @proc.call(f)end ["puts 1+2", "puts 3+4", "puts 5+6"].each do |f|  proc_eval(f)end # Output :# 3# 7# 11
 
Last edited by a moderator:

TheoAllen

Self-proclaimed jack of all trades
Veteran
Joined
Mar 16, 2012
Messages
5,592
Reaction score
6,522
First Language
Indonesian
Primarily Uses
RMVXA
So that's mean each eval formula is saved in different Proc?

Because the formula will be anything

if that is the case, then proc_eval should be defined like this

Code:
def proc_eval(f)  @proc ||= {}  @proc[f] ||= eval("lambda { #{f}} ")  @proc[f].callend
And it seems work
 
Last edited by a moderator:

Tsukihime

Veteran
Veteran
Joined
Jun 30, 2012
Messages
8,564
Reaction score
3,846
First Language
English
I'm assuming the formula does not change during the game.
 

TheoAllen

Self-proclaimed jack of all trades
Veteran
Joined
Mar 16, 2012
Messages
5,592
Reaction score
6,522
First Language
Indonesian
Primarily Uses
RMVXA
The formula won't change since it's a part of user config

Just it's not only one formula. It could be many.
 

Tsukihime

Veteran
Veteran
Joined
Jun 30, 2012
Messages
8,564
Reaction score
3,846
First Language
English
That doesn't really have anything to do with a formula that could be "anything" and therefore require you to call eval inside your proc.
 
Last edited by a moderator:

TheoAllen

Self-proclaimed jack of all trades
Veteran
Joined
Mar 16, 2012
Messages
5,592
Reaction score
6,522
First Language
Indonesian
Primarily Uses
RMVXA
By "anything" I means that the eval formula could be two, three, or even ten formula depends of how the user set the config

Anyway, I already got the answer
 

Users Who Are Viewing This Thread (Users: 0, Guests: 1)

Latest Threads

Latest Posts

Latest Profile Posts

Couple hours of work. Might use in my game as a secret find or something. Not sure. Fancy though no? :D
Holy stink, where have I been? Well, I started my temporary job this week. So less time to spend on game design... :(
Cartoonier cloud cover that better fits the art style, as well as (slightly) improved blending/fading... fading clouds when there are larger patterns is still somewhat abrupt for some reason.
Do you Find Tilesetting or Looking for Tilesets/Plugins more fun? Personally I like making my tileset for my Game (Cretaceous Park TM) xD
How many parameters is 'too many'??

Forum statistics

Threads
105,865
Messages
1,017,059
Members
137,575
Latest member
akekaphol101
Top