Formula optimization: speed up your formula eval code!

● ARCHIVED · READ-ONLY
Started by Tsukihime 16 posts View original ↗
  1. 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?
  2. Good info thanks.
  3. 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?
  4. 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).
  5. TheoAllen said:
    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.
  6. 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
  7. 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.
  8. 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
  9. What would the purpose of the eval inside the proc be?
  10. 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
  11. 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
  12. I'm assuming the formula does not change during the game.
  13. The formula won't change since it's a part of user config

    Just it's not only one formula. It could be many.
  14. 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.
  15. 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