Macro Best Practices

Discuss macro implementations, ask for macro help (to share your creations, see User Creations, probably either Campaign Frameworks or Drop-in Resources).

Moderators: dorpond, trevor, Azhrei, giliath, jay, Mr.Ice

User avatar
Azhrei
Site Admin
Posts: 12086
Joined: Mon Jun 12, 2006 1:20 pm
Location: Tampa, FL

Re: Macro Best Practices

Post by Azhrei »

Barrodin wrote:EDIT: On a side note, some sort of built in timestamp function would be enourmously helpful in tests like this.
You're new here, so I'll forgive you.. ;) :mrgreen:

There is the Wiki: getInfo() function, as well as turning on logging (read the README that's part of the zip file download of MapTool).

User avatar
CoveredInFish
Demigod
Posts: 3104
Joined: Mon Jun 29, 2009 10:37 am
Location: Germany
Contact:

Re: Macro Best Practices

Post by CoveredInFish »

I've also build some time measuring functions using Wiki: getInfo() that you can find following the link in my sig. Its a dropin package of macros for benchmarking (big word for little function, but anyway).

Barrodin
Kobold
Posts: 16
Joined: Thu Aug 18, 2011 10:51 am

Re: Macro Best Practices

Post by Barrodin »

Ah! Bless you both. I was starting from the documentation wiki end of things and turning up nothing searching for time, timestamp, etc.

Give me a bit to add in some better logging to my script and I'll post some hard results on how much time the various methods of passing JSON objects take (assuming anyone is interested).

Barrodin
Kobold
Posts: 16
Joined: Thu Aug 18, 2011 10:51 am

Re: Macro Best Practices

Post by Barrodin »

Well, this turned out to be a bit interesting. It turns out that when you really pare everything down to the bare bones, so that you are only testing the time it takes to pass a JSON object in or out of a user defined function, the way you pass it makes a very big difference (at least for large objects). The results are below, and it is just as undeniably clearcut as I could have hoped for.

Passing/returning a JSON Object by name and using eval to copy the value into a variable has essentially the same cost in time regardless of how large the JSON Object becomes. On the other hand, passing/returning the object itself to/from a user defined function slows down tremendously for large objects.

This only really applies for large JSON Objects though, for small JSON Objects it really doesn't seem to matter. For arrays smaller than size 256/512, there really wasn't a meaningful difference in the time it took.
The Results
Image
And the raw numbers, if anyone is that curious:
Raw numbers
Pass by Value
203, 188, 187, 204, 187, 203, 250, 281, 375, 578, 985, 1813, 3578, 6953, 13750, 27719
187, 187, 188, 187, 203, 219, 234, 297, 391, 578, 953, 1782, 3563, 6921, 13766, 27750
187, 188, 172, 187, 188, 204, 234, 281, 375, 594, 968, 1766, 3547, 6921, 13687, 27547
187, 188, 187, 188, 203, 219, 234, 281, 391, 593, 969, 1782, 3563, 6937, 13750, 27563

Pass by Name
203, 203, 203, 203, 204, 188, 203, 204, 203, 203, 203, 187, 203, 203, 203, 203
203, 203, 203, 188, 203, 203, 204, 203, 187, 187, 187, 203, 203, 203, 203, 203
188, 203, 203, 203, 203, 203, 188, 188, 203, 203, 203, 187, 187, 204, 187, 204
203, 204, 203, 203, 204, 187, 187, 187, 187, 188, 188, 188, 188, 188, 203, 203

Return by Value
297, 297, 297, 297, 297, 297, 328, 375, 438, 609, 922, 1563, 2829, 5390, 10937, 23500
297, 281, 297, 281, 297, 312, 313, 360, 453, 594, 907, 1563, 2828, 5360, 10765, 23641
297, 281, 281, 281, 297, 297, 313, 375, 437, 594, 907, 1563, 2829, 5375, 10766, 23484
296, 282, 282, 281, 297, 312, 313, 360, 453, 594, 922, 1563, 2828, 5390, 10922, 23531

Return by Name
312, 329, 312, 313, 312, 313, 312, 313, 296, 297, 296, 297, 297, 297, 312, 313
297, 312, 313, 312, 313, 297, 312, 313, 297, 312, 313, 297, 312, 312, 312, 297
297, 313, 312, 313, 312, 313, 312, 297, 313, 312, 313, 312, 312, 312, 312, 312
313, 312, 313, 312, 297, 313, 312, 297, 297, 313, 312, 297, 297, 297, 312, 312

User avatar
aliasmask
RPTools Team
Posts: 9023
Joined: Tue Nov 10, 2009 6:11 pm
Location: Bay Area

Re: Macro Best Practices

Post by aliasmask »

I'm curious to see the test code you used to run this test. How many keys were in the object and how big was the data?

Barrodin
Kobold
Posts: 16
Joined: Thu Aug 18, 2011 10:51 am

Re: Macro Best Practices

Post by Barrodin »

aliasmask wrote:I'm curious to see the test code you used to run this test. How many keys were in the object and how big was the data?
The initial object was a JSON Array with a single entry (0).

On each loop, that JSON Array was merged with itself, doubling the size of the object to be passed (hence the *2^n notation on the object size axis). On the last loop the array would have been 65536 entries long, all of which would be identical. At some point I'll try it with more complex objects to see if it affects the times any, but not for now I think.

Code to follow shortly, just need to load up Maptool.

EDIT: Code. Nothing special right away, just the usual function definitions in onCampaignLoad. The only thing of note here is that test_passvalue() is the only function defined to create a new scope when it is called.
onCampaignLoad@this
[code=php][h: defineFunction("test_passvalue", "test_passvalue@this", 0, 1)]
[h: defineFunction("test_passname", "test_passname@this", 0, 0)]
[h: defineFunction("test_returnvalue", "test_returnvalue@this", 0, 0)]
[h: defineFunction("test_returnname", "test_returnname@this", 0, 0)] [/code]
Here's the meat of it, not that it's a lot to look at. The inner loop needs to be changed slightly for each test as follows: (since I just wanted a quick test I just edited it by hand each time).
Individual Test Edits
To test pass by value:
[code=php][h, COUNT(testCount): test_passvalue(testObject)] [/code]

To test pass by name:
[code=php][h, COUNT(testCount): test_passname("testObject")] [/code]

To test return by value:
[code=php][h, COUNT(testCount), CODE: {
 [h: tempVal = test_returnvalue("testObject")]
 [h: returnVal = tempVal]
}] [/code]


To test return by name:
[code=php][h, COUNT(testCount), CODE: {
 [h: tempVal = test_returnname("testObject")]
 [h: returnVal = eval(tempVal)]
}] [/code]
test_series@this
[code=php][h: testObject = json.append("[]", 0)]
[h: testMaxSizeExp = 16]
[h: testCount = 100]
[h: timeResults = ""]

[h, COUNT(testMaxSizeExp), CODE: {
  [h: testObject = json.merge(testObject, testObject)]
  [h: preTime = json.get(getInfo("client"), "timeInMs")]
  [h, COUNT(testCount): test_passname("testObject")]
  [h: postTime = json.get(getInfo("client"), "timeInMs")]
  [h: timeResults = listAppend(timeResults, postTime - preTime)]
}]

[r: timeResults] [/code]
Then the various test functions, which are all very, very simple two line functions that just take a value or return a value.
Test Functions
[code=php]
... test_passname() ...
[h: testedObject = eval(arg(0))]
[h: macro.return = 1]

... test_passvalue() ...
[h: testedObject = arg(0)]
[h: macro.return = 1]

... test_returnname() ...
[h: testedObject = eval(arg(0))]
[h: macro.return = "testedObject"]

... test_returnvalue() ...
[h: testedObject = eval(arg(0))]
[h: macro.return = testedObject]
 [/code]

User avatar
aliasmask
RPTools Team
Posts: 9023
Joined: Tue Nov 10, 2009 6:11 pm
Location: Bay Area

Re: Macro Best Practices

Post by aliasmask »

Barrodin wrote:
aliasmask wrote:I'm curious to see the test code you used to run this test. How many keys were in the object and how big was the data?
The initial object was a JSON Array with a single entry (0).

On each loop, that JSON Array was merged with itself, doubling the size of the object to be passed (hence the *2^n notation on the object size axis). On the last loop the array would have been 65536 entries long, all of which would be identical. At some point I'll try it with more complex objects to see if it affects the times any, but not for now I think.

Code to follow shortly, just need to load up Maptool.
Cool. I've done some testing myself here and discovered, but didn't really emphasis it, that passing the large array slowed down my testing.

Something just occurred to me. Since you're just passing the name then evaluating, does that mean you're using a UDF where it doesn't have it's own scope? I suppose if the UDF didn't know which variable to process from the parent then that would be useful, otherwise you could just use the actual variable inside the UDF without having to eval. I'm just trying to think of an instance where this could be useful to me. How did you run across the need to do this?

Barrodin
Kobold
Posts: 16
Joined: Thu Aug 18, 2011 10:51 am

Re: Macro Best Practices

Post by Barrodin »

aliasmask wrote:Something just occurred to me. Since you're just passing the name then evaluating, does that mean you're using a UDF where it doesn't have it's own scope?
Yep, the trick revolves around keeping the JSON object in the local scope instead of passing it into a new scope (which, because of the way UDF's work, means transforming it into a string and then back again each time).
aliasmask wrote:I suppose if the UDF didn't know which variable to process from the parent then that would be useful, otherwise you could just use the actual variable inside the UDF without having to eval.
Yeah, the main thing I saw this being used for is moving bits of reusable code out into their own functions without the performance hit for passing JSON objects. I chose to do the whole eval and assign deal because I specifically didn't want it to require me to remember the name of variables -inside- the function when I was writing code that called the function. By passing a variable name and evaluating it inside the function (and vice versa on the return), I can avoid either side having to have "magic names" for the input and output variable.
aliasmask wrote:I'm just trying to think of an instance where this could be useful to me. How did you run across the need to do this?
Honestly? I'm just a bit of an odd duck. I was working on a dice roller that involves JSON objects, and the idea of it being even a miniscule fraction of a second slower bothered me (after all, I might someday be using that dice roller library for something that needs random rolls on a much larger scale, so I'd rather plan ahead a bit.)


Barrodin
Kobold
Posts: 16
Joined: Thu Aug 18, 2011 10:51 am

Re: Macro Best Practices

Post by Barrodin »

wolph42 wrote:you could have saved yourself the time by reading this: Speed_Up_Your_Macros
Eh, that doesn't cover what we were fooling with here. While that covers some methods of storing and retrieving a JSON being faster or slower than others (and is part of what made me wonder about this whole subject in the first place), it doesn't cover this name + eval() method of passing a JSON object to a local-scope user defined function.

User avatar
wolph42
Winter Wolph
Posts: 9999
Joined: Fri Mar 20, 2009 5:40 am
Location: Netherlands
Contact:

Re: Macro Best Practices

Post by wolph42 »

Barrodin wrote:
wolph42 wrote:you could have saved yourself the time by reading this: Speed_Up_Your_Macros
Eh, that doesn't cover what we were fooling with here. While that covers some methods of storing and retrieving a JSON being faster or slower than others (and is part of what made me wonder about this whole subject in the first place), it doesn't cover this name + eval() method of passing a JSON object to a local-scope user defined function.
*rereads (better this time)*... indeed, my bad, in that case, maybe you want to update that part of the wiki with your findings!!

User avatar
davout
Cave Troll
Posts: 78
Joined: Wed Oct 12, 2011 2:28 am

Re: Macro Best Practices

Post by davout »

Barrodin wrote: Keep in mind that any variables in a function will overwrite functions in the macro from which it is called, so be conservative creating new variables and always use a distinctive name (I tend to add the prefix local_ to the variable names inside such functions, as a reminder). If you do decide to use this trick it is probably best to create as few variables inside the fucntion as possible, to avoid the possibility of collisions.
Barrodin,

I'm trying to wrap my head around this. Are you saying that if I had the following code, the output of varX would be 20 and not 10?

Code: Select all

[h: testObject = json.set("{}", "Key 1", value1, "Key 2", value2, ... , "Key N", valueN)]
[h: varX = 10]
[h: test_function("testObject")]
[varX]

... Inside test_function ...
[h: local_testObject = eval(arg(0))]
[h: varX = 20]
Thanks.

User avatar
Bone White
Great Wyrm
Posts: 1124
Joined: Tue Aug 23, 2011 11:41 am
Location: Cornwall, UK

Re: Macro Best Practices

Post by Bone White »

davout wrote:
Barrodin wrote: Keep in mind that any variables in a function will overwrite functions in the macro from which it is called, so be conservative creating new variables and always use a distinctive name (I tend to add the prefix local_ to the variable names inside such functions, as a reminder). If you do decide to use this trick it is probably best to create as few variables inside the fucntion as possible, to avoid the possibility of collisions.
Barrodin,

I'm trying to wrap my head around this. Are you saying that if I had the following code, the output of varX would be 20 and not 10?

Code: Select all

[h: testObject = json.set("{}", "Key 1", value1, "Key 2", value2, ... , "Key N", valueN)]
[h: varX = 10]
[h: test_function("testObject")]
[varX]

... Inside test_function ...
[h: local_testObject = eval(arg(0))]
[h: varX = 20]
Thanks.
As far as I understand it, it all depends if you defined test_function with newScope = 1 or newScope = 0. See Wiki: defineFunction()

To further clarify,:
when defining test_function if newScope is set to true(1), then in your example varX will be output as being 10.
when defining test_function if newScope is set to false(0), then in your example varX will be output as 20.

When using newScope as true, you can only return items through macro.return.

corrected to be correct
Last edited by Bone White on Sat Oct 15, 2011 10:18 am, edited 1 time in total.

User avatar
davout
Cave Troll
Posts: 78
Joined: Wed Oct 12, 2011 2:28 am

Re: Macro Best Practices

Post by davout »

newScope - If the defined function should create a new variable scope when executed, defaults to true(1). A new variable scope means that the defined function can only read the variables of the macro that called it; if you do not create a new scope the defined function can read, update, and create variables in its parent's variable scope. Updating variables in the parent's scope includes over-writing any parameters that a parent might have stored within arg(), if a user-defined function that does not create a new scope is called within another user-defined function.
Ok now what aliasmask and Barrodin said makes since.
aliasmask wrote:Since you're just passing the name then evaluating, does that mean you're using a UDF where it doesn't have it's own scope?
Barrodin wrote:I chose to do the whole eval and assign deal because I specifically didn't want it to require me to remember the name of variables -inside- the function when I was writing code that called the function. By passing a variable name and evaluating it inside the function (and vice versa on the return), I can avoid either side having to have "magic names" for the input and output variable.
:shock: Oh my that is major mind shift from other languages.

Thanks.

User avatar
Rumble
Deity
Posts: 6235
Joined: Tue Jul 01, 2008 7:48 pm

Re: Macro Best Practices

Post by Rumble »

Bone White wrote:
davout wrote:
Barrodin wrote: Keep in mind that any variables in a function will overwrite functions in the macro from which it is called, so be conservative creating new variables and always use a distinctive name (I tend to add the prefix local_ to the variable names inside such functions, as a reminder). If you do decide to use this trick it is probably best to create as few variables inside the fucntion as possible, to avoid the possibility of collisions.
Barrodin,

I'm trying to wrap my head around this. Are you saying that if I had the following code, the output of varX would be 20 and not 10?

Code: Select all

[h: testObject = json.set("{}", "Key 1", value1, "Key 2", value2, ... , "Key N", valueN)]
[h: varX = 10]
[h: test_function("testObject")]
[varX]

... Inside test_function ...
[h: local_testObject = eval(arg(0))]
[h: varX = 20]
Thanks.
As far as I understand it, it all depends if you defined test_function with ignoreOutput = 1 or ignoreOutput = 0. See Wiki: defineFunction()

To further clarify,:
when defining test_function if ignoreOutput is set to true(1), then in your example varX will be output as being 10.
when defining test_function if ignoreOutput is set to false(0), then in your example varX will be output as 20.

When using ignoreOutput as true, you can only return items through macro.return.

Wouldn't it depend on whether it was a new scope, not whether it was ignore output? That is, if you have varX in one macro, and call a new macro with the same scope and assign a new value to varX, then varX will change. If the called macro has a new scope, then varX in the called macro has no affect on varX in the calling macro. Or did I miss something earlier?

Post Reply

Return to “Macros”