• Hello [name]! Thanks for joining the GMC. Before making any posts in the Tech Support forum, can we suggest you read the forum rules? These are simple guidelines that we ask you to follow so that you can get the best help possible for your issue.

Discussion Suggestion: Allow C++ native datatypes/C++ code injection in YYC projects (benefits with examples within)

vdweller

Member
Hi all,

This suggestion has been made in passing in other threads, more like wishful thinking. I am aware that the timing is rather bad and now with 2.3 the dev team will have to iron out many things before even considering adding more features. Today I would like to provide a series of examples where the ability to declare C++ native datatypes or inject C++ code from within the IDE can greatly speed up execution time in certain scenarios. I hope the examples provided will illustrate my point and will spark constructive discussion. I will also be very happy to be proven wrong in any or all of my arguments: Maybe I have overlooked something or maybe what I proposed is simply not feasible. In any case, valuable conclusions can be reached. I am using v2.2.5 but I suspect not much will have changed in the infrastructure discussed below (and if it has changed, chances are it has changed to be even slower, at least for the moment!).

I will be using the following premise: At some point in everyone's code, a data structure will have to be iterated via some loop. Maybe it is a large grid, maybe it is a list. Due to the way variables are stored in GML, even for incrementing a single variable, using the vanilla GML variables (declared with var, for example), is way slower than using a native C++ datatype.

The benefits of my proposal are not restricted to iterations. For example, calculating simple values may also be faster, like calculating a heuristic in an A* algorithm.

Apparently all values (strings or numbers) are stored in a YYRValue class instance. This class has overloaded operators: For instance, the C++ generated code can correctly handle adding a YYRValue and an int datatype, which is precisely the foundation of this proposal.

Let's write a trivial script in GameMaker and examine the generated YYC code:

GML:
var a=16;

print(a);

Output: 16

C++:
#include <YYGML.h>
#include "gmlids.h"
YYRValue& gml_Script_print( CInstance* pSelf, CInstance* pOther, YYRValue& _result, int _count,  YYRValue** _args  );
#ifndef __YYNODEFS
#else
#endif // __YYNODEFS

YYRValue& gml_Script_script0( CInstance* pSelf, CInstance* pOther, YYRValue& _result, int _count,  YYRValue** _args  )
{
YY_STACKTRACE_FUNC_ENTRY( "gml_Script_script0", 0 );
YYRValue local_a;
YYRValue __ret1__(0);

_result = 0;

YY_STACKTRACE_LINE(1);
local_a=16;

YY_STACKTRACE_LINE(4);
FREE_RValue( &__ret1__ );
YYRValue* __pArg8__[]={&/* local */local_a};
gml_Script_print(pSelf,pOther,__ret1__,1,__pArg8__);
return _result;
}

What if we modify the generated C++ code by adding a double directly to the YYRValue?

C++:
#include <YYGML.h>
#include "gmlids.h"
YYRValue& gml_Script_print( CInstance* pSelf, CInstance* pOther, YYRValue& _result, int _count,  YYRValue** _args  );
#ifndef __YYNODEFS
#else
#endif // __YYNODEFS

YYRValue& gml_Script_script0( CInstance* pSelf, CInstance* pOther, YYRValue& _result, int _count,  YYRValue** _args  )
{
YY_STACKTRACE_FUNC_ENTRY( "gml_Script_script0", 0 );
YYRValue local_a;
YYRValue __ret1__(0);

_result = 0;

YY_STACKTRACE_LINE(1);
local_a=16;

double a = 2.25;
local_a+=a;

YY_STACKTRACE_LINE(4);
FREE_RValue( &__ret1__ );
YYRValue* __pArg8__[]={&/* local */local_a};
gml_Script_print(pSelf,pOther,__ret1__,1,__pArg8__);
return _result;
}

Output: 18.25

Sure enough, the underlying C++ code knows how to handle such an addition and it produces the correct result. I have also tested this with ints as well as other operations like multiplication.

Now let's go to a somewhat more complex example. Suppose we have a large ds_grid and we want each cell to be its column index + row index + some number (a double in this example).

GML:
var t=get_timer();

var len=1000;
var b=14.27;

var grid=ds_grid_create(len,len);

for (var i=0;i<len;++i) {
    for (var j=0;j<len;++j) {
        grid[#i,j]=i+j+b;
    }
}

global.totaltime+=get_timer()-t;


if (global.first==0) { //just to print grid data and verify result consistency
    for (var a=0;a<len;++a) {
        for (var b=0;b<len;++b) {
            print(grid[#a,b]);
        }
    }
}

++global.first;


ds_grid_destroy(grid);

Time: 164 ms

The generated C++ code is the following:

C++:
#include <YYGML.h>
#include "gmlids.h"
extern YYVAR g_FUNC_get_timer;
extern YYVAR g_FUNC_ds_grid_destroy;
#ifndef __YYNODEFS
#else
#endif // __YYNODEFS

YYRValue& gml_Script_script0( CInstance* pSelf, CInstance* pOther, YYRValue& _result, int _count,  YYRValue** _args  )
{
YY_STACKTRACE_FUNC_ENTRY( "gml_Script_script0", 0 );
YYRValue local_t;
YYRValue local_len;
YYRValue local_b;
YYRValue local_grid;
YYRValue local_i;
YYRValue local_j;
YYRValue & global_totaltime = ((CInstanceBase*)g_pGlobal)->GetYYVarRef(kVARID_global_totaltime);
YYRValue __ret1__(0);

_result = 0;

YY_STACKTRACE_LINE(1);
FREE_RValue( &__ret1__ );
local_t=YYGML_CallLegacyFunction(pSelf,pOther,__ret1__,0,g_FUNC_get_timer.val,NULL);

YY_STACKTRACE_LINE(3);
local_len=1000;

YY_STACKTRACE_LINE(4);
local_b=14.27;

YY_STACKTRACE_LINE(6);
local_grid=YYGML_ds_grid_create((int)((int)/* local */local_len.asReal()),(int)((int)/* local */local_len.asReal()));

YY_STACKTRACE_LINE(8);

YY_STACKTRACE_LINE(8);
local_i=0;
bool ___f8___ = true;
while( true ) {
if (!___f8___) {

YY_STACKTRACE_LINE(8);
++/* local */local_i;
}
___f8___ = false;
bool ___b9___ = ((/* local */local_i < /* local */local_len));
if (!___b9___) break;
{

YY_STACKTRACE_LINE(8);

YY_STACKTRACE_LINE(9);

YY_STACKTRACE_LINE(9);
local_j=0;
bool ___f10___ = true;
while( true ) {
if (!___f10___) {

YY_STACKTRACE_LINE(9);
++/* local */local_j;
}
___f10___ = false;
bool ___b11___ = ((/* local */local_j < /* local */local_len));
if (!___b11___) break;
{

YY_STACKTRACE_LINE(9);

YY_STACKTRACE_LINE(10);
YYGML_ds_grid_set((int)((int)/* local */local_grid.asReal()),(int)((int)/* local */local_i.asReal()),(int)((int)/* local */local_j.asReal()),((/* local */local_i + /* local */local_j) + /* local */local_b));
}
}
}
}

YY_STACKTRACE_LINE(14);
FREE_RValue( &__ret1__ );
/* First usage */global_totaltime+=(YYGML_CallLegacyFunction(pSelf,pOther,__ret1__,0,g_FUNC_get_timer.val,NULL) - /* local */local_t);

YY_STACKTRACE_LINE(29);
FREE_RValue( &__ret1__ );
YYRValue* __pArg12__[]={&/* local */local_grid};
YYGML_CallLegacyFunction(pSelf,pOther,__ret1__,1,g_FUNC_ds_grid_destroy.val,__pArg12__);
return _result;
}

Wordy, eh? Well, can't help it.

For now, take a look at the YYGML_ds_grid_set function. It takes 4 arguments:
  • The grid ID (internally stored as an int)
  • The column index (also int)
  • The row index (also int)
  • The value to set (probably a YYRValue)
There are a few things to notice about this implementation. First, there seems to be an unnecessary amount of int casts. Second, the first 3 arguments essentially use YYRValues converted to ints.

But what if we could use ints - or doubles - in the first place?

(I removed the /* local */ comments for clarity - look between stacktrace lines 8 and 14 for the changes)

GML:
#include <YYGML.h>
#include "gmlids.h"
YYRValue& gml_Script_print( CInstance* pSelf, CInstance* pOther, YYRValue& _result, int _count,  YYRValue** _args  );
extern YYVAR g_FUNC_get_timer;
extern YYVAR g_FUNC_ds_grid_destroy;
#ifndef __YYNODEFS
#else
#endif // __YYNODEFS

YYRValue& gml_Script_script0( CInstance* pSelf, CInstance* pOther, YYRValue& _result, int _count,  YYRValue** _args  )
{
YY_STACKTRACE_FUNC_ENTRY( "gml_Script_script0", 0 );
YYRValue local_t;
YYRValue local_len;
YYRValue local_b;
YYRValue local_grid;
YYRValue local_i;
YYRValue local_j;
YYRValue & global_totaltime = ((CInstanceBase*)g_pGlobal)->GetYYVarRef(kVARID_global_totaltime);
YYRValue & global_first = ((CInstanceBase*)g_pGlobal)->GetYYVarRef(kVARID_global_first);
YYRValue local_a;
YYRValue __ret1__(0);
YYRValue __ret2__(0);

_result = 0;

YY_STACKTRACE_LINE(1);
FREE_RValue( &__ret1__ );
local_t=YYGML_CallLegacyFunction(pSelf,pOther,__ret1__,0,g_FUNC_get_timer.val,NULL);

YY_STACKTRACE_LINE(3);
local_len=1000;

YY_STACKTRACE_LINE(4);
local_b=14.27;

YY_STACKTRACE_LINE(6);
local_grid=YYGML_ds_grid_create((int)((int)local_len.asReal()),(int)((int)local_len.asReal()));

YY_STACKTRACE_LINE(8);

int i=0,j=0;
int len=(int)local_len;
int grid=(int)local_grid.asReal();

YY_STACKTRACE_LINE(8);
i=0;
bool ___f1___ = true;
while( true ) {
if (!___f1___) {

YY_STACKTRACE_LINE(8);
++i;
}
___f1___ = false;
bool ___b2___ = ((i < len));
if (!___b2___) break;
{

YY_STACKTRACE_LINE(8);

YY_STACKTRACE_LINE(9);

YY_STACKTRACE_LINE(9);
j=0;
bool ___f3___ = true;
while( true ) {
if (!___f3___) {

YY_STACKTRACE_LINE(9);
++j;
}
___f3___ = false;
bool ___b4___ = ((j < len));
if (!___b4___) break;
{

YY_STACKTRACE_LINE(9);

YY_STACKTRACE_LINE(10);
YYGML_ds_grid_set(grid,i,j,(i+j) + local_b);
}
}
}
}

YY_STACKTRACE_LINE(14);
FREE_RValue( &__ret1__ );
/* First usage */global_totaltime+=(YYGML_CallLegacyFunction(pSelf,pOther,__ret1__,0,g_FUNC_get_timer.val,NULL) - local_t);

YY_STACKTRACE_LINE(16);
if((/* First usage */global_first == 0)) {

YY_STACKTRACE_LINE(16);

YY_STACKTRACE_LINE(17);

YY_STACKTRACE_LINE(17);
local_a=0;
bool ___f5___ = true;
while( true ) {
if (!___f5___) {

YY_STACKTRACE_LINE(17);
++local_a;
}
___f5___ = false;
bool ___b6___ = ((local_a < local_len));
if (!___b6___) break;
{

YY_STACKTRACE_LINE(17);

YY_STACKTRACE_LINE(18);

YY_STACKTRACE_LINE(18);
local_b=0;
bool ___f7___ = true;
while( true ) {
if (!___f7___) {

YY_STACKTRACE_LINE(18);
++local_b;
}
___f7___ = false;
bool ___b8___ = ((local_b < local_len));
if (!___b8___) break;
{

YY_STACKTRACE_LINE(18);

YY_STACKTRACE_LINE(19);
FREE_RValue( &__ret1__ );
FREE_RValue( &__ret2__ );
YYRValue* __pArg9__[]={&YYGML_ds_grid_get(__ret2__,local_grid,(int)((int)local_a.asReal()),(int)((int)local_b.asReal()))};
gml_Script_print(pSelf,pOther,__ret1__,1,__pArg9__);
}
}
}
}
}

YY_STACKTRACE_LINE(24);
++global_first;

YY_STACKTRACE_LINE(28);
FREE_RValue( &__ret1__ );
YYRValue* __pArg10__[]={&local_grid};
YYGML_CallLegacyFunction(pSelf,pOther,__ret1__,1,g_FUNC_ds_grid_destroy.val,__pArg10__);
return _result;
}


Here I added the following things:
  • Declared an i and j int to use for iteration.
  • Declared the loop length (len) as an int.
  • Stored the grid id as an int outside of the loop (grid).
So instead of incrementing and comparing YYRValues, the while loop bodies compare pure ints! But...is each cell value calculated correctly? Yes, it is!

Time: 103 ms

With this change our code is about 40% faster. This is not a negligible increase. It doesn't fall into the micro-optimization realm. In a scenario where we have to loop through a large collection, this is a substantial increase in performance.

Even better? Declare a double b and set its value to the value of YYRValue local_b (the 14.27 number we add to each cell), and add b in the grid function instead of local_b. The time then plummets to 67 milliseconds! 40% of the vanilla GML code time!

So my suggestion is either to be able to directly declare native C++ datatypes in GML, something like
GML:
int i = 10;
or
Code:
var i = $10;
And have the code be converted appropriately, or perhaps inject C++ code as a comment:
GML:
/*C++
blah blah C++ code...
*/
And have the commented and specially tagged code added properly to the generated C++ code. We can already alter the generated .cpp files but this is more of a weak hack, as any such change is gone with clearing the cache and recompiling.

I am aware that this is a can of worms for the dev team. There may be many caveats and using such native datatypes may not work under many circumstances. But in small, concentrated pieces of code the advantages are very obvious and if the staff is wary that there will be lots of tech support requests of the "I TRIED X AND DIDN'T WORK" persuasion, well, this is why in programs some features can be declared 'experimental', followed by a 'use at your own risk' disclaimer ;)
 
Last edited:
Top