-
Notifications
You must be signed in to change notification settings - Fork 7
OptimizingModels
This page aims at presenting some tips to optimize the memory footprint or the execution time of a model in GAMA.
Note: since GAMA 1.6.1, some optimizations have become obsolete because they have been included in the compiler. They have, then, been removed from this page. For instance, writing 'rgb(0,0,0)' is now compiled directly as '#black'.
If you have a species of agents that, once created, are not supposed to do anything more (i.e. no behavior, no reflex, their actions triggered by other agents, their attributes being simply read and written by other agents), such as a "data" grid, or agents representing a "background" (from a shape file, etc.), consider using the schedules: []
facet on the definition of their species. This trick allows to tell GAMA to not schedule any of these agents.
grid my_grid height: 100 width: 100 schedules: [] {
...
}
The schedules
facet is dynamically computed (even if the agents are not scheduled), so, if you happen to define agents that only need to be scheduled every x cycles, or depending on a condition, you can also write schedules
to implement this. For instance, the following species will see its instances scheduled every 10 steps and only if a certain condition is met:
species my_species schedules: (every 10) ? (condition ? my_species : []) : [] {
...
}
In the same way, modelers can use the frequency facet to define when the agents of a species are going to be activated. By setting this facet to 0, the agents are never activated.
species my_species frequency: 0 {
...
}
In this section, we present some facets that allow to optimize the use of grid (in particular in terms of memories). Note that all these facet can be combined (see the Life model from the Models library).
If false, then a special class of agents is used. This special class of agents used less memories but has some limitation: the agents cannot inherit from a "normal" species, they cannot have sub-populations, their name cannot be modified, etc.
grid cell width: 50 height: 50 use_regular_agents: false ;
If false, then only one geometry is used for all agents. This facet allows to gain a lot of memory, but should not be used if the geometries of the agents are often activated (for instance, by an aspect).
grid cell width: 50 height: 50 use_individual_shapes: false ;
The grid
statement can also specify whether the agents of the grid are computed in parallel, using the facet parallel
. This could increase (depending on the computation) the execution time.
It is sometimes necessary to randomly select an element of a list that verifies a given condition.
Many modelers use the one_of
and the where
operators to do this:
bug one_big_bug <- one_of (bug where (each.size > 10));
Whereas it is often more optimized to use the shuffle
operator to shuffle the list, then the first_with
operator to select the first element that verifies the condition:
bug one_big_bug <- shuffle(bug) first_with (each.size > 10);
It is quite common to want to count the number of elements of a list or a container that verify a condition. The obvious to do it is:
int n <- length(my_container where (each.size > 10));
This will however create an intermediary list before counting it, and this operation can be time consuming if the number of elements is important. To alleviate this problem, GAMA includes an operator called count
that will count the elements that verify the condition by iterating directly on the container (no useless list created):
int n <- my_container count (each.size > 10);
Several spatial query operators (such as closest_to
, at_distance
, overlapping
or inside
) allow to restrict the agents being queried to a container of agents. For instance, one can write:
agent closest_agent <- a_container_containing_agents closest_to self;
This expression is formally equivalent to :
agent closest_agent <- a_container_containing_agent with_min_of (each distance_to self);
But it is much faster if your container is large, as it will query the agents using a spatial index (instead of browsing through the whole container). Note that in some cases, when you have a small number of agents, the first syntax will be faster. The same applies to the other operators.
Now consider a very common case: you need to restrict the agents being queried, not to a container, but to a species (which, actually, acts as a container in most cases). For instance, you want to know which predator is the closest to the current agent. If we apply the pattern above, we would write:
predator closest_predator <- predator with_min_of (each distance_to self);
or
predator closest_predator <- list(predator) closest_to self;
But these two operators can be painfully slow if your species has many instances (even in the second form). In that case, always prefer using directly the species as the left member:
predator closest_ predator <- predator closest_to self;
Not only is the syntax clearer, but the speed gain can be phenomenal because, in that case, the list of instances is not used (we just check if the agent is an instance of the left species).
However, what happens if one wants to query instances belonging to 2 or more species? If we follow our reasoning, the immediate way to write it would be (if predator 1 and predator 2 are two species):
agent closest_agent <- (list(predator1) + list(predator2)) closest_to self;
or, more simply:
agent closest_agent <- (predator1 + predator2) closest_to self;
The first syntax suffers from the same problem than the previous syntax: GAMA has to browse through the list (created by the concatenation of the species populations) to filter agents. The solution, then, is again to use directly the species, as GAMA is clever enough to create a temporary "fake" population out of the concatenation of several species, which can be used exactly like a list of agents, but provides the advantages of a species population (no iteration made during filtering).
The closest_to
operator can sometimes be slow if numerous agents are concerned by this query. If the modeler is just interested in a small subset of agents, it is possible to apply a first spatial filtering on the agent list by using the at_distance
operator.
For example, if the modeler wants first to do a spatial filtering of 10m:
agent closest_agent <- (predator1 at_distance 10) closest_to self;
To be sure to find an agent, the modeler can use a test statement:
agent closest_agent <- (predator1 at_distance 10) closest_to self;
if (closest_agent = nil) {closest_agent <- predator1 closest_to self;}
It is quite common to want to display an agent as a circle or a square. A common mistake is to mix up the shape to draw and the geometry of the agent in the model. If the modeler just wants to display a particular shape, he/she should not modify the agent geometry (i.e. its shape
attribute, which is a point by default), but just specify the shape to draw in the agent aspect.
species bug {
int size <- rnd(100);
aspect circle {
draw circle(2) color: #blue;
}
}
Note that in OpenGL (3D) and Java2D (2D), the two rendering subsystems used in GAMA, creating and drawing a circle geometry is more time consuming than creating and drawing a square (or a rectangle). In the same way, drawing a sphere is more time consuming than drawing a cube. Hence, if you want to optimize your model displays and if the rendering does not explicitly need "rounded" agents, try to use squares/cubes rather than circles/spheres.
In OpenGL display, it is possible to specify that it is not necessary to refresh a layer with the facet refresh
. If in a species, the properties used for visualization (location, shape or color) are never modified, you can set refresh
to false. Example:
display city_display_opengl type: opengl{
species building aspect: base refresh: false;
species road aspect: base refresh: false;
species people aspect: base;
}
Manipulating containers (lists, maps etc.) and agents are usually the core of a model and the most critical parts of the code in terms of performances.
It is possible to execute the reflexes of agents of a species in parallel threads. This can greatly improve the execution time of a model as by default agents are executed one after another. To activate parallel execution of agents you just need to set the parallel
facet of the species
(or grid
) to true:
species dummy_species parallel:true{
}
grid my_grid parallel:true{
}
Note: By default this option is not activated because we cannot guaranty the reproducibility of an experiment if it is. It implies that we do not know in advance which agent is going to be executed first, this also means that if your agents are meant to be executed in a certain order, this could break your model. Take for example this model where each cell of a grid has an effect on its neighbouring cells ,here disabling them, each disabled cell will be represented by the red color and enabled cells by the green color:
model parallel
grid my_grid parallel:false width:5 height:5 {
bool to_be_executed <- true;
rgb color <- #green update:to_be_executed ? #green : #red;
reflex cancel_neighbours {
if to_be_executed{
write "I am cell '" + name + "' and my reflexes are executed";
loop c over:neighbors{
c.to_be_executed <- false;
}
}
to_be_executed <- true; // we reset for the next cycle
}
}
experiment a {
output{
display main type:2d antialias:false{
grid my_grid border:#black;
}
}
}
With parallel set to false
(the default) the order of execution of each cell is always the same and can be predicted to yield this display after the first cycle:
But if you change its value to true
you can end up with this:
And it can change every cycle (or not) and there's no way to know in advance how it's going to look.
In gama there are multiple ways of iterating over containers and agents:
- the
ask
statement to iterate over agents - the
loop
statement to iterate over anything, with its multiple syntaxes:-
times
iterates a certain number of times -
over
iterates directly over a list -
from
andto
provide an index to iterate in a range of values -
until
iterates until a certain condition is met
-
- the different container operators (such as
collect
orwhere
) provide shortcuts for generic list manipulation tasks
In general the specialized operators (see this page in the documentation for the full list) are way more efficient, followed by ask
and the loop over
, then comes the other loop syntaxes.
So as a general rule of thumb you should use as much as possible the container-related operators instead of the generic loop
and ask
statements.
Here is an example model that showcases the difference in execution time of the different methods to sum the value of a property of all the agents in the simulation:
model accessinglistitems
global{
int nb_agents <- 50000;
}
species b{
int v;
init {
v <- rnd(0, 10);
}
}
experiment e {
parameter "number of agents" var:nb_agents;
reflex fill_list_from_agents {
write "Start benchmarking with " + nb_agents + " agents";
// we reset the agents
ask b{
do die;
}
create b number:nb_agents;
int s1 <- 0;
benchmark "sum with loop over" repeat:100{
s1 <- 0;
loop obj over:b{
s1 <- s1 + obj.v;
}
}
int s2 <- 0;
benchmark "sum with loop from to" repeat:100{
int to <- length(b)-1;
s2 <- 0;
loop i from:0 to:to{
s2 <- s2 + b[i].v;
}
}
int s3 <- 0;
benchmark "sum with loop times" repeat:100{
int to <- length(b);
int i <- 0;
s3 <- 0;
loop times:to{
s3 <- s3 + b[i].v;
i <- i + 1;
}
}
int s4 <- 0;
benchmark "sum with ask" repeat:100{
s4 <- 0;
ask b{
s4 <- s4 + v;
}
}
int s5 <- 0;
benchmark "sum with collect" repeat:100{
s5 <- sum(b collect (each.v));
}
// we check that all methods yield the same result
assert s1 = s2;
assert s2 = s3;
assert s3 = s4;
assert s4 = s5;
}
}
Which gives results similar to this:
String manipulation, and especially concatenation (adding two strings together) is harmless for performances in normal condition, but once strings become big enough (thousands of characters), every operation becomes extremely costly. Moreover, the execution time of concatenation seem to be an exponential function of the size of the string. Take for example this model:
model stringconcat
experiment test {
reflex concat {
string s;
int nb_concat <- 100;
loop times:nb_concat{
s <- s + rnd(0,10);
}
}
}
We are simply concatenating random digits into a string, ending up with a 100 character string.
So far so good, this is good enough and works flawlessly on most machines. Now change nb_concat
to 100000
and the operation starts to be non-negligible (for example it takes more than half a second on my computer).
We can modify a bit the model to have a precise value of how long it takes:
model stringconcat
experiment test {
action concatenate_string(int nb){
string s <- '';
loop times:nb{
s <- s + rnd(0,10);
}
}
reflex concat {
benchmark "concatening 100 000 times" repeat:10 {
do concatenate_string(100000);
}
benchmark "concatening 200 000 times" repeat:10 {
do concatenate_string(200000);
}
benchmark "concatening 300 000 times" repeat:10 {
do concatenate_string(300000);
}
}
}
With that model you should notice that 200 000 characters is significantly slower than 100 000, in my case it was more than 4 times slower, and 300 000 even more as it was almost 10 times slower. Results on my computer looked like this:
This can be particularly annoying because creating big string is itself a way to optimize outputing data from a model (it is often faster to write a big string once than small strings many times).
To help with string concatenation, it is advised to use the concatenate operator: instead of concatenating many times your text into one string, append each small string into a list of string, and when all the component are collected, merge them only once with the operator. Let's modify the previous model to see how it works:
model stringconcat
experiment test {
action concatenate_string(int nb){
list<string> list_strings <- [];
loop times:nb{
list_strings <+ string(rnd(0,10));
}
string s <- concatenate(list_strings);
}
reflex concat {
benchmark "concatening 100 000 times" repeat:10 {
do concatenate_string(100000);
}
benchmark "concatening 200 000 times" repeat:10 {
do concatenate_string(200000);
}
benchmark "concatening 300 000 times" repeat:10 {
do concatenate_string(300000);
}
}
}
On the same computer as in the previous example I get those results:
Not only it is orders of magnitude faster, but it is also growing linearly instead of exponentially.
- Installation and Launching
- Workspace, Projects and Models
- Editing Models
- Running Experiments
- Running Headless
- Preferences
- Troubleshooting
- Introduction
- Manipulate basic Species
- Global Species
- Defining Advanced Species
- Defining GUI Experiment
- Exploring Models
- Optimizing Models
- Multi-Paradigm Modeling
- Manipulate OSM Data
- Cleaning OSM Data
- Diffusion
- Using Database
- Using FIPA ACL
- Using BDI with BEN
- Using Driving Skill
- Manipulate dates
- Manipulate lights
- Using comodel
- Save and restore Simulations
- Using network
- Headless mode
- Using Headless
- Writing Unit Tests
- Ensure model's reproducibility
- Going further with extensions
- Built-in Species
- Built-in Skills
- Built-in Architecture
- Statements
- Data Type
- File Type
- Expressions
- Exhaustive list of GAMA Keywords
- Installing the GIT version
- Developing Extensions
- Introduction to GAMA Java API
- Using GAMA flags
- Creating a release of GAMA
- Documentation generation