Difference between revisions of "Agent Calls"

(Strategy for optimizing dotnet Agent Calls)
m (Object test)
 
(76 intermediate revisions by 3 users not shown)
Line 1: Line 1:
 
[[Category:Compiler]]
 
[[Category:Compiler]]
Work in progress!
+
[[Category:Code Generation]]
== Terminology ==
+
{{Warning|Article under development}}
In the following text the place where an agent is created (with the agent keyword or the tilde operator) is called the '''agent creation'''. The feature to which the agent points is the '''agent callee''' and the place (or places) where the agent is called is denoted '''agent call'''.  
+
 
 +
====Introduction====
 +
The Eiffel programming language supports closures with agents. As a reminder, an agent is a normal Eiffel object of one of the types:
 +
*FUNCTION
 +
*PROCEDURE
 +
Some terminology is needed: The place where an agent is created (with the agent keyword or the tilde operator) is called the '''agent creation'''. The feature to which the agent points is the '''agent callee''' and the place (or places) where the agent is called is denoted '''agent call'''.  
 +
 
 +
An example that clarifies these terms:
  
<code>[eiffel,N]
+
{|border="0" cellpadding="2" cellspacing="0" align="center"
class  
+
|-valign="top" -halign="center"
 +
|
 +
<code>[eiffel,N]class  
 
     TERMINOLOGY
 
     TERMINOLOGY
  
Line 25: Line 34:
 
         do
 
         do
 
         end
 
         end
end
+
end</code>
 +
|
 +
|}
  
</code>
+
====The problem====
 +
It is again helpful to focus on the differences between the Eiffel and C language. Eiffel supports closures, C only supports function pointers. A way is needed thus to construct closures based on function pointers. Dynamic binding also contributes its part to the problem. It is not known at compile time which exact feature an agent (when looking at its '''agent creation''') refers to. But it gets even worse, sometimes the feature is not known until the agent is called (at runtime). This is the case for agents with open targets.
 +
====The data structure====
 +
Class C will be used for the following examples:
  
== Wrapper generation ==
+
{|border="0" cellpadding="2" cellspacing="0" align="center"
==== Frozen code ====
+
|-valign="top" -halign="center"
Lets look first at frozen code. Class C is used throughout the following examples.
+
|
<code>[eiffel,N]
+
<code>[eiffel,N]class
class
+
 
     C
 
     C
  
Line 41: Line 54:
 
             ...
 
             ...
 
         end   
 
         end   
end  
+
end</code>
 +
|
 +
|}
 +
 
 +
There are three data structures needed for an agent:
 +
* The open_map (member of class ROUTINE)
 +
* The closed_operands (member of class ROUTINE)
 +
* The open_operands
 +
The open_map is known at compile time when an '''agent_creation''' is detected. The closed_operands are known at '''agent creation''' time. The open_operands are not known until the agent is called.
 +
 
 +
The content of these structures will be given for agent one and two:
 +
 
 +
{|border="0" cellpadding="2" cellspacing="0" align="center"
 +
|-valign="top" -halign="center"
 +
! Agent one !! Agent two
 +
|-
 +
|
 +
<code>[eiffel, N]
 +
agent c.f (?, 1, ?)
 
</code>
 
</code>
 +
|
 +
<code>[eiffel, N]
 +
agent {C}.f (1, ?, "hello")
 +
</code>
 +
|}
 +
 +
The open_map is an array that has an entry for every open parameter. Its content for agents one and two are:
 +
#{2,4}
 +
#{1,3}
 +
The closed_operands is a TUPLE that has an entry for every closed operand. Its content for agents one and two again:
 +
#{c, 1}
 +
#{1, "hello"}
 +
 +
====The wrapper====
 +
It comes by no surprise that the compiler creates special wrapper functions for agents. It is interesting though that these wrappers are created per '''agent creation'''. This has the advantage that it is known whether a given formal argument is open or closed.
  
For everly '''agent creation''' a c-function is generated. This wrapper function does the following:
+
In workbench mode, for every '''agent creation''' a C-function is generated. This wrapper function does the following:
*Reorder the closed and open arguments.
+
*Arrange the closed and open arguments in the right order.
 
*Calculate the proper '''agent callee''' based on the dynamic type of the target.
 
*Calculate the proper '''agent callee''' based on the dynamic type of the target.
*Call the '''agent callee'''
+
*Call the '''agent callee'''.
  
 
For the '''agent creation''':
 
For the '''agent creation''':
 +
{|border="0" cellpadding="2" cellspacing="0" align="center"
 +
|-valign="top" -halign="center"
 +
|
 
<code>[eiffel, N]
 
<code>[eiffel, N]
 
             agent c.f (?, 1, ?)
 
             agent c.f (?, 1, ?)
 
</code>
 
</code>
 +
|
 +
|}
 
the following wrapper function is generated:
 
the following wrapper function is generated:
<code>[c, N]
+
{|border="0" cellpadding="2" cellspacing="0" align="center"
EIF_REFERENCE _fAaatpmf_2_4 (EIF_REFERENCE (*f_ptr)(EIF_REFERENCE, EIF_INTEGER_32, EIF_INTEGER_32, EIF_REFERENCE),  
+
|-valign="top" -halign="center"
                            EIF_TYPED_ELEMENT* closed, EIF_TYPED_ELEMENT* open)
+
|<code>[c, N]EIF_REFERENCE _fAaatpmf_2_4 (
 +
  EIF_REFERENCE (*f_ptr)(EIF_REFERENCE, //can be ignored
 +
                          EIF_INTEGER_32,  
 +
                          EIF_INTEGER_32,  
 +
                          EIF_REFERENCE),  
 +
  EIF_TYPED_ELEMENT* closed,           //Closed arguments
 +
  EIF_TYPED_ELEMENT* open)             //Open arguments
 
{
 
{
     return (FUNCTION_CAST(EIF_REFERENCE, (EIF_REFERENCE, EIF_INTEGER_32, EIF_INTEGER_32, EIF_REFERENCE))  
+
     return  
        RTVF(350, 30, "f", closed [1].element.rarg))(
+
      (FUNCTION_CAST(EIF_REFERENCE, (EIF_REFERENCE,  
             closed [1].element.rarg,  
+
                                    EIF_INTEGER_32,  
 +
                                    EIF_INTEGER_32,  
 +
                                    EIF_REFERENCE))  
 +
      //Dynamic binding using the target: 
 +
      RTVF(350, 30, "f", closed [1].element.rarg))(
 +
      //Arranging the open and closed arguments:
 +
             closed [1].element.rarg,                  
 
             open [1].element.i32arg,  
 
             open [1].element.i32arg,  
 
             closed [2].element.i32arg,  
 
             closed [2].element.i32arg,  
Line 66: Line 129:
 
}
 
}
 
</code>
 
</code>
 +
|
 +
|}
 +
This example shows how the wrapper does its three tasks. The suffix "_2_4" of the wrapper comes from the fact that the second and forth parameter (first and third argument) are open.
  
And for this '''agent creation''':
+
A second example is given:
<code>[eiffel, N]
+
{|border="0" cellpadding="2" cellspacing="0" align="center"
 +
|-valign="top" -halign="center"
 +
|<code>[eiffel, N]
 
             agent {C}.f (1, ?, "hello")
 
             agent {C}.f (1, ?, "hello")
 
</code>
 
</code>
The wrapper function looks like:
+
|}
 
+
For this '''agent creation''' the wrapper function looks like:
<code>[c, N]
+
{|border="0" cellpadding="2" cellspacing="0" align="center"
EIF_REFERENCE _fAaatpmf_1_3 (EIF_REFERENCE (*f_ptr)(EIF_REFERENCE, EIF_INTEGER_32, EIF_INTEGER_32, EIF_REFERENCE),  
+
|-valign="top" -halign="center"
                            EIF_TYPED_ELEMENT* closed, EIF_TYPED_ELEMENT* open)
+
|<code>[c, N]
 +
EIF_REFERENCE _fAaatpmf_1_3 (
 +
  EIF_REFERENCE (*f_ptr)(EIF_REFERENCE, //can be ignored
 +
                          EIF_INTEGER_32,  
 +
                          EIF_INTEGER_32,  
 +
                          EIF_REFERENCE),  
 +
  EIF_TYPED_ELEMENT* closed,             //Closed arguments
 +
  EIF_TYPED_ELEMENT* open)               //Open arguments
 
{
 
{
    return (FUNCTION_CAST(EIF_REFERENCE, (EIF_REFERENCE, EIF_INTEGER_32, EIF_INTEGER_32, EIF_REFERENCE))  
+
  return  
        RTVF(350, 30, "f", open [1].element.rarg))(
+
      (FUNCTION_CAST(EIF_REFERENCE, (EIF_REFERENCE,  
            open [1].element.rarg,  
+
                                    EIF_INTEGER_32,  
            closed [1].element.i32arg,  
+
                                    EIF_INTEGER_32,  
            open [2].element.i32arg,  
+
                                    EIF_REFERENCE))  
            closed [2].element.rarg);
+
      //Dynamic binding using the target: 
 +
      RTVF(350, 30, "f", open [1].element.rarg))(
 +
      //Arranging the open and closed arguments:
 +
        open [1].element.rarg,  
 +
        closed [1].element.i32arg,  
 +
        open [2].element.i32arg,  
 +
        closed [2].element.rarg);
 
}
 
}
 
</code>
 
</code>
 +
|}
 +
This time the suffix is "_1_3" since the first and third parameter (target and second argument) are open.
  
====Finalized code====
 
In finalized code things are only slightly different.
 
  
Whenever an agent is created with a closed target, the actual '''agent callee''' is calculated at '''agent creation''' time.
 
Moreover, two wrappers are generated:
 
*one which expects the closed arguments as a tuple (encapsulated). The only difference to the frozen wrapper is how the '''agent callee''' is calculated.
 
*and one that expects them as separate parameters. We will later see how this wrapper is used to optimize some special agent calls.
 
  
For the '''agent creation''':
+
====Agent call====
<code>[eiffel, N]
+
An '''agent call''' like the following:
            agent c.f (?, 1, ?)
+
{|border="0" cellpadding="2" cellspacing="0" align="center"
</code>
+
|-valign="top" -halign="center"
the following wrapper functions are generated:
+
|
<code>[c, N]
+
<eiffel>
EIF_REFERENCE _fAaatpmf_2_4 (EIF_REFERENCE (*f_ptr)(EIF_REFERENCE, EIF_INTEGER_32, EIF_INTEGER_32, EIF_REFERENCE),  
+
local
                            EIF_TYPED_ELEMENT* closed, EIF_TYPED_ELEMENT* open)
+
  f: FUNCTION [ANY, TUPLE [INTEGER], INTEGER]
{
+
  b: BOOLEAN
    return f_ptr (closed [1].element.rarg, open [1].element.i32arg, closed [2].element.i32arg, open [2].element.rarg);
+
do
}
+
  b := f.item ([10])
 +
end
 +
</eiffel>
 +
|
 +
|}
 +
Obviously calls feature <eiffel>item</eiffel> of class <eiffel>FUNCTION</eiffel>:
  
EIF_REFERENCE __fAaatpmf_2_4 (EIF_REFERENCE (*f_ptr)(EIF_REFERENCE, EIF_INTEGER_32, EIF_INTEGER_32, EIF_REFERENCE),  
+
{|border="0" cellpadding="2" cellspacing="0" align="center"
                              EIF_TYPED_ELEMENT* closed, EIF_INTEGER_32 op_2, EIF_REFERENCE op_4)
+
|-valign="top" -halign="center"
{
+
|
    return f_ptr (closed [1].element.rarg, op_2, closed [2].element.i32arg, op_4);
+
<eiffel>
}
+
item (args: OPEN_ARGS): RESULT_TYPE is
</code>
+
  -- Result of calling function with `args' as operands.
The name of the wrapper for the encapsulated closed arguments allways starts with one '_', whereas the other one starts with two underlines.
+
  require
 +
      valid_operands: valid_operands (args)
 +
  local
 +
      l_closed_count: INTEGER
 +
  do
 +
      l_closed_count :=  closed_operands.count
 +
      Result := fast_item (encaps_rout_disp, calc_rout_addr,  
 +
            $closed_operands, $args, class_id, feature_id,
 +
            is_precompiled, is_basic, is_inline_agent,  
 +
            l_closed_count, open_count, $open_map)
 +
  end
 +
</eiffel>
 +
|
 +
|}
  
== Melted agent creation ==
+
This leads to a call to the external C feature <eiffel>fast_item</eiffel> of class <eiffel>FUNCTION</eiffel>:
Of course it is possible that the agent creation is melted and hence there is no wrapper. For this situation the Eiffel runtime provides two generic wrapper functions:
+
  
<code>[c, N]
+
{|border="0" cellpadding="2" cellspacing="0" align="center"
RT_LNK void rout_obj_call_procedure_dynamic (
+
|-valign="top" -halign="center"
    int stype_id, int feature_id, int is_precompiled, int is_basic_type, int is_inline_agent, EIF_TYPED_ELEMENT* closed_args,
+
|
    int closed_count, EIF_TYPED_ELEMENT* open_args, int open_count, EIF_REFERENCE open_map);
+
<eiffel>fast_item (a_rout_disp, a_calc_rout_addr: POINTER
 
+
  a_closed_operands: POINTER; a_operands: POINTER
RT_LNK void rout_obj_call_function_dynamic (
+
  a_class_id, a_feature_id: INTEGER
    int stype_id, int feature_id, int is_precompiled, int is_basic_type, int is_inline_agent, EIF_TYPED_ELEMENT* closed_args,
+
          a_is_precompiled, a_is_basic, a_is_inline_agent: BOOLEAN
    int closed_count, EIF_TYPED_ELEMENT* open_args, int open_count, EIF_REFERENCE open_map, void* res);
+
  a_closed_count, a_open_count: INTEGER; a_open_map: POINTER): RESULT_TYPE
</code>
+
 
+
These functions calculate the '''agent callee''' with the static type id and a feature id. Furthermore the reordering of open and closed arguments has to be done at agent call time with help of the open_map.
+
 
+
== Agent call ==
+
To see how an '''agent call''' works it helps to look at the fast_item feature of class FUNCTION. In workbench mode, whenever
+
there is a wrapper function (a_rout_disp /= Void) it is used to call the '''agent callee'''. If not, the generic wrapper (rout_obj_call_function_dynamic) from the runtime is called. In finalized mode there is allways a wrapper.
+
 
+
<code>[eiffel, N]
+
    fast_item (a_rout_disp, a_calc_rout_addr: POINTER
+
      a_closed_operands: POINTER; a_operands: POINTER
+
      a_class_id, a_feature_id: INTEGER; a_is_precompiled, a_is_basic, a_is_inline_agent: BOOLEAN
+
      a_closed_count, a_open_count: INTEGER; a_open_map: POINTER): RESULT_TYPE
+
 
external
 
external
 
    "C inline use %"eif_rout_obj.h%""
 
    "C inline use %"eif_rout_obj.h%""
Line 145: Line 226:
 
    $$_result_type result;
 
    $$_result_type result;
 
    if ($a_rout_disp != 0) {
 
    if ($a_rout_disp != 0) {
return (FUNCTION_CAST($$_result_type, (EIF_POINTER, EIF_REFERENCE, EIF_REFERENCE)) $a_rout_disp)(
+
return (FUNCTION_CAST($$_result_type,  
 +
                            (EIF_POINTER, EIF_REFERENCE, EIF_REFERENCE)) $a_rout_disp)(
 
    $a_calc_rout_addr, $a_closed_operands, $a_operands);
 
    $a_calc_rout_addr, $a_closed_operands, $a_operands);
 
    } else {
 
    } else {
rout_obj_call_function_dynamic (
+
...
        $a_class_id,
+
    $a_feature_id,
+
    $a_is_precompiled,
+
    $a_is_basic,
+
    $a_is_inline_agent,
+
    $a_closed_operands,
+
    $a_closed_count,
+
    $a_operands,
+
    $a_open_count,
+
    $a_open_map,
+
    &result);
+
return result;
+
 
    }
 
    }
 
#else
 
#else
    return (FUNCTION_CAST($$_result_type, (EIF_POINTER, EIF_REFERENCE, EIF_REFERENCE)) $a_rout_disp)(
+
    ...
$a_calc_rout_addr, $a_closed_operands, $a_operands);
+
 
#endif
 
#endif
 
    ]"
 
    ]"
end
+
end</eiffel>
</code>
+
|
 +
|}
  
==== Agent calls in final code ====
+
When there is a valid function pointer to the wrapper (<c>a_rout_disp != 0</c>) it is called with the closed and open operands as parameters. The wrapper will then call the actual '''agent callee'''. The case when there is no valid function pointer will be considered later.
In finalized mode the compiler further optimizes '''agent calls''' if the following conditions hold:
+
*The call is qualified and the target of the call is of type FUNCTION, PROCEDURE or PREDICATE.
+
*The '''agent call''' is done with a call to item or call.
+
  
In this case, the compiler generates a direct call to the wrapper function.  
+
====Improving the dynamic binding====
 +
When at '''agent creation''' the target is closed, the exact '''agent callee''' can be calculated at '''agent creation'''. This improves performance when the agent is called more than once. This optimization is only done when in final mode.
  
Quite often an '''agent call''' is done with a manifest tuple:
+
====Eradicating manifest tuple creation====
 +
In finalized mode there is a simple optimization that speeds things up significantly. It is not uncommon, that an '''agent calls''' passes a manifest tuple as parameter, see the following call:
  
<code>[eiffel,N]
+
{|border="0" cellpadding="2" cellspacing="0" align="center"
caller (f: FUNCTION [ANY, TUPLE [INTEGER, STRING], STRING]): STRING
+
|-valign="top" -halign="center"
    do
+
|
Result := f.item ([1, "hello"])
+
<eiffel>
    end
+
  b := f.item ([10, 20, "hello"])
</code>
+
</eiffel>
 +
|
 +
|}
  
This calls are further optimized by not generating the tuple but passing the elements of the tuple directly to the wrapper function. Thats why we need the second wrapper function in finalized mode. Feature caller is translated to the following c-code:
+
The lifetime of this tuple is short. It is only used throughout the call to <eiffel>item</eiffel>. Tuple creations are not that fast, they take several times longer than the '''agent call''' itself. Thats why the EiffelStudio in finalized mode replaces these calls to <eiffel>item</eiffel> by a direct call to wrapper function. If the open parameters in Eiffel code are passed in the form of a manifest tuple, they are directly passed to the wrapper function in the translated code, to get rid of the slow tuple creation.
  
<code>[c,N]
+
{|border="0" cellpadding="2" cellspacing="0" align="center"
EIF_REFERENCE Fbnoo0v (EIF_REFERENCE Current, EIF_REFERENCE arg1)
+
|-valign="top" -halign="center"
 +
|
 +
<c>
 +
tp1 = RTMS_EX_H("hello",5,1702403183);
 +
tp2 = tp1;
 +
tp1 = (FUNCTION_CAST(EIF_REFERENCE, (EIF_POINTER,
 +
                                    EIF_REFERENCE,
 +
                                    EIF_INTEGER_32,
 +
                                    EIF_INTEGER_32,
 +
                                    EIF_REFERENCE))
 +
      *(EIF_POINTER *)(loc3+ @PTROFF(5,4,0,3,0,0)))(    //calling the wrapper function
 +
        *(EIF_POINTER *)(loc3+ @PTROFF(5,4,0,3,0,1)),  //passing the calculated agent callee
 +
*(EIF_REFERENCE *)(loc3 + @REFACS(1)),        //passing the closed parameters
 +
        ((EIF_INTEGER_32) 10L),                        //passing 10
 +
        ((EIF_INTEGER_32) 11L),                        //passing 11
 +
        tp2);                                          //passing "hello"
 +
</c>
 +
|
 +
|}
 +
 
 +
====Wrapper functions in final mode====
 +
Due to the potential optimization described above, in final mode two C wrapper functions are generated:
 +
*one which expects the closed arguments as a tuple (encapsulated). The only difference to the frozen wrapper is how the '''agent callee''' is calculated.
 +
*and one that expects them as separate parameters.
 +
 
 +
For the '''agent creation''':
 +
{|border="0" cellpadding="2" cellspacing="0" align="center"
 +
|-valign="top" -halign="center"
 +
|
 +
<eiffel>
 +
agent c.f (?, 1, ?)
 +
</eiffel>
 +
|}
 +
the following wrapper functions are generated:
 +
{|border="0" cellpadding="2" cellspacing="0" align="center"
 +
|-valign="top" -halign="center"
 +
|
 +
<c>
 +
EIF_REFERENCE _fAaatpmf_2_4 (
 +
  EIF_REFERENCE (*f_ptr)(EIF_REFERENCE,  
 +
                          EIF_INTEGER_32,
 +
                          EIF_INTEGER_32,
 +
                          EIF_REFERENCE),
 +
  EIF_TYPED_ELEMENT* closed,
 +
  EIF_TYPED_ELEMENT* open)
 
{
 
{
     GTCX
+
     return f_ptr (closed [1].element.rarg,
    EIF_REFERENCE tp1 = NULL;
+
                  open [1].element.i32arg,
 +
                  closed [2].element.i32arg,
 +
                  open [2].element.rarg);
 +
}
 +
</c>
 +
|
 +
<c>
 +
EIF_REFERENCE __fAaatpmf_2_4 (
 +
  EIF_REFERENCE (*f_ptr)(EIF_REFERENCE,
 +
                          EIF_INTEGER_32,
 +
                          EIF_INTEGER_32,
 +
                          EIF_REFERENCE),
 +
  EIF_TYPED_ELEMENT* closed,
 +
  EIF_INTEGER_32 op_2,
 +
  EIF_REFERENCE op_4)
 +
{
 +
    return f_ptr (closed [1].element.rarg,
 +
                  op_2,
 +
                  closed [2].element.i32arg,
 +
                  op_4);
 +
}
 +
</c>
 +
|}
 +
The name of the wrapper for the encapsulated closed arguments always starts with one '_', whereas the other one starts with two underlines.
  
    tp1 = (FUNCTION_CAST(EIF_REFERENCE, (EIF_POINTER, EIF_REFERENCE, EIF_INTEGER_32, EIF_BOOLEAN)) *(EIF_POINTER *)(
+
====Melted agent creator====
        arg1+ @PTROFF(5,4,0,3,0,0)))(
+
Of course it is possible that the '''agent creation''' is melted and hence there is no wrapper. For this situation the Eiffel runtime provides two generic wrapper functions:
    *(EIF_POINTER *)(arg1+ @PTROFF(5,4,0,3,0,1)),
+
 
    *(EIF_REFERENCE *)(arg1 + @REFACS(1)), ((EIF_INTEGER_32) 1L), (EIF_BOOLEAN) 1);
+
<code>[c, N]
+
RT_LNK void rout_obj_call_procedure_dynamic (
    return (EIF_REFERENCE)tp1;
+
    int stype_id, int feature_id, int is_precompiled, int is_basic_type, int is_inline_agent, EIF_TYPED_ELEMENT* closed_args,
}
+
    int closed_count, EIF_TYPED_ELEMENT* open_args, int open_count, EIF_REFERENCE open_map);
 +
 
 +
RT_LNK void rout_obj_call_function_dynamic (
 +
    int stype_id, int feature_id, int is_precompiled, int is_basic_type, int is_inline_agent, EIF_TYPED_ELEMENT* closed_args,  
 +
    int closed_count, EIF_TYPED_ELEMENT* open_args, int open_count, EIF_REFERENCE open_map, void* res);
 
</code>
 
</code>
 +
 +
These functions calculate the '''agent callee''' with the static type id and a feature id. Furthermore the reordering of open and closed arguments has to be done at agent call time with help of the open_map.
  
 
== Strategy for optimizing dotnet Agent Calls ==
 
== Strategy for optimizing dotnet Agent Calls ==
 +
Work in progress!
 
Compared to the Classic version, dotnet Agent Calls are slow. The a short look at feature apply of class FUNCTION shows why:
 
Compared to the Classic version, dotnet Agent Calls are slow. The a short look at feature apply of class FUNCTION shows why:
  
<code>[eiffel,N]
+
<e>
    apply  
+
apply  
-- Call function with `operands' as last set.
+
        -- Call function with `operands' as last set.
        do
+
    do
    last_result ?= rout_disp.invoke (target_object, internal_operands)
+
        if attached {RESULT_TYPE} rout_disp.invoke (target_object, internal_operands) as al_last_result then
 +
            last_result := al_last_result
 
         end
 
         end
</code>
+
    end
 +
</e>
 +
 
 +
The actual call is done through reflection. Additionally, in dotnet the two major optimizations are missing:
 +
 
 +
*Directly passing the elements of manifest tuples as parameters. (And hence not creating the tuple)
 +
*Computing the reordering of closed and open operands at compile time. (By generating the proper wrapper)
 +
 
 +
All these three issues can be removed.
 +
 
 +
For every '''agent creation''' we create:
 +
* A delegate type that requires two Tuples (closed and open arguments) as arguments.
 +
* A static method that conforms to this delegate type. This method does the ordering of closed and open arguments and calls the '''agent callee'''.
 +
* A second delegate type, that requires one Tuple (closed arguments) and all the open arguments as arguments.
 +
* A second static method which conforms to the second delegate type and does the ordering and '''agent callee''' call.

Latest revision as of 10:46, 1 February 2014

Warning.png Warning: Article under development

Introduction

The Eiffel programming language supports closures with agents. As a reminder, an agent is a normal Eiffel object of one of the types:

  • FUNCTION
  • PROCEDURE

Some terminology is needed: The place where an agent is created (with the agent keyword or the tilde operator) is called the agent creation. The feature to which the agent points is the agent callee and the place (or places) where the agent is called is denoted agent call.

An example that clarifies these terms:

class
    TERMINOLOGY
 
feature
 
    f1
        local
            p: PROCEDURE [ANY, TUPLE]
        do
            p := agent target   -- agent creation
        end
 
    f2 (p: PROCEDURE [ANY, TUPLE])
        do
            p.call ([])         -- agent call
        end
 
    target                      -- callee
        do
        end
end

The problem

It is again helpful to focus on the differences between the Eiffel and C language. Eiffel supports closures, C only supports function pointers. A way is needed thus to construct closures based on function pointers. Dynamic binding also contributes its part to the problem. It is not known at compile time which exact feature an agent (when looking at its agent creation) refers to. But it gets even worse, sometimes the feature is not known until the agent is called (at runtime). This is the case for agents with open targets.

The data structure

Class C will be used for the following examples:

class
    C
 
feature
    f (p1, p2: INTEGER; p3: STRING): STRING
        do
            ...
        end   
end

There are three data structures needed for an agent:

  • The open_map (member of class ROUTINE)
  • The closed_operands (member of class ROUTINE)
  • The open_operands

The open_map is known at compile time when an agent_creation is detected. The closed_operands are known at agent creation time. The open_operands are not known until the agent is called.

The content of these structures will be given for agent one and two:

Agent one Agent two
agent c.f (?, 1, ?)
agent {C}.f (1, ?, "hello")

The open_map is an array that has an entry for every open parameter. Its content for agents one and two are:

  1. {2,4}
  2. {1,3}

The closed_operands is a TUPLE that has an entry for every closed operand. Its content for agents one and two again:

  1. {c, 1}
  2. {1, "hello"}

The wrapper

It comes by no surprise that the compiler creates special wrapper functions for agents. It is interesting though that these wrappers are created per agent creation. This has the advantage that it is known whether a given formal argument is open or closed.

In workbench mode, for every agent creation a C-function is generated. This wrapper function does the following:

  • Arrange the closed and open arguments in the right order.
  • Calculate the proper agent callee based on the dynamic type of the target.
  • Call the agent callee.

For the agent creation:

agent c.f (?, 1, ?)

the following wrapper function is generated:

EIF_REFERENCE _fAaatpmf_2_4 (
   EIF_REFERENCE (*f_ptr)(EIF_REFERENCE, //can be ignored 
                          EIF_INTEGER_32, 
                          EIF_INTEGER_32, 
                          EIF_REFERENCE), 
   EIF_TYPED_ELEMENT* closed,            //Closed arguments
   EIF_TYPED_ELEMENT* open)              //Open arguments
{
    return 
      (FUNCTION_CAST(EIF_REFERENCE, (EIF_REFERENCE, 
                                     EIF_INTEGER_32, 
                                     EIF_INTEGER_32, 
                                     EIF_REFERENCE)) 
      //Dynamic binding using the target:  
      RTVF(350, 30, "f", closed [1].element.rarg))(  
      //Arranging the open and closed arguments:
            closed [1].element.rarg,                    
            open [1].element.i32arg, 
            closed [2].element.i32arg, 
            open [2].element.rarg);
}

This example shows how the wrapper does its three tasks. The suffix "_2_4" of the wrapper comes from the fact that the second and forth parameter (first and third argument) are open.

A second example is given:

agent {C}.f (1, ?, "hello")

For this agent creation the wrapper function looks like:

EIF_REFERENCE _fAaatpmf_1_3 (
   EIF_REFERENCE (*f_ptr)(EIF_REFERENCE,  //can be ignored 
                          EIF_INTEGER_32, 
                          EIF_INTEGER_32, 
                          EIF_REFERENCE), 
   EIF_TYPED_ELEMENT* closed,             //Closed arguments
   EIF_TYPED_ELEMENT* open)               //Open arguments
{
   return 
      (FUNCTION_CAST(EIF_REFERENCE, (EIF_REFERENCE, 
                                     EIF_INTEGER_32, 
                                     EIF_INTEGER_32, 
                                     EIF_REFERENCE)) 
      //Dynamic binding using the target:  
      RTVF(350, 30, "f", open [1].element.rarg))(
      //Arranging the open and closed arguments:
         open [1].element.rarg, 
         closed [1].element.i32arg, 
         open [2].element.i32arg, 
         closed [2].element.rarg);
}

This time the suffix is "_1_3" since the first and third parameter (target and second argument) are open.


Agent call

An agent call like the following:

local
   f: FUNCTION [ANY, TUPLE [INTEGER], INTEGER]
   b: BOOLEAN
do
   b := f.item ([10])
end

Obviously calls feature item of class FUNCTION:

item (args: OPEN_ARGS): RESULT_TYPE is
   -- Result of calling function with `args' as operands.
   require
      valid_operands: valid_operands (args)
   local
      l_closed_count: INTEGER
   do
      l_closed_count :=  closed_operands.count
      Result := fast_item (encaps_rout_disp, calc_rout_addr, 
            $closed_operands, $args, class_id, feature_id,
            is_precompiled, is_basic, is_inline_agent, 
            l_closed_count, open_count, $open_map)
   end

This leads to a call to the external C feature fast_item of class FUNCTION:

fast_item (a_rout_disp, a_calc_rout_addr: POINTER
	   a_closed_operands: POINTER; a_operands: POINTER
	   a_class_id, a_feature_id: INTEGER
           a_is_precompiled, a_is_basic, a_is_inline_agent: BOOLEAN
	   a_closed_count, a_open_count: INTEGER; a_open_map: POINTER): RESULT_TYPE
	external
	    "C inline use %"eif_rout_obj.h%""
	alias
	    "[
                #ifdef WORKBENCH
		    $$_result_type result;
		    if ($a_rout_disp != 0) {
			return (FUNCTION_CAST($$_result_type, 
                            (EIF_POINTER, EIF_REFERENCE, EIF_REFERENCE)) $a_rout_disp)(
			    $a_calc_rout_addr, $a_closed_operands, $a_operands);
		    } else {
			...
		    }
		#else
		    ...
		#endif
	    ]"
	end

When there is a valid function pointer to the wrapper (a_rout_disp != 0) it is called with the closed and open operands as parameters. The wrapper will then call the actual agent callee. The case when there is no valid function pointer will be considered later.

Improving the dynamic binding

When at agent creation the target is closed, the exact agent callee can be calculated at agent creation. This improves performance when the agent is called more than once. This optimization is only done when in final mode.

Eradicating manifest tuple creation

In finalized mode there is a simple optimization that speeds things up significantly. It is not uncommon, that an agent calls passes a manifest tuple as parameter, see the following call:

b := f.item ([10, 20, "hello"])

The lifetime of this tuple is short. It is only used throughout the call to item. Tuple creations are not that fast, they take several times longer than the agent call itself. Thats why the EiffelStudio in finalized mode replaces these calls to item by a direct call to wrapper function. If the open parameters in Eiffel code are passed in the form of a manifest tuple, they are directly passed to the wrapper function in the translated code, to get rid of the slow tuple creation.

tp1 = RTMS_EX_H("hello",5,1702403183);
tp2 = tp1;
tp1 = (FUNCTION_CAST(EIF_REFERENCE, (EIF_POINTER, 
                                     EIF_REFERENCE, 
                                     EIF_INTEGER_32, 
                                     EIF_INTEGER_32, 
                                     EIF_REFERENCE)) 
      *(EIF_POINTER *)(loc3+ @PTROFF(5,4,0,3,0,0)))(    //calling the wrapper function
         *(EIF_POINTER *)(loc3+ @PTROFF(5,4,0,3,0,1)),  //passing the calculated agent callee
	 *(EIF_REFERENCE *)(loc3 + @REFACS(1)),         //passing the closed parameters
         ((EIF_INTEGER_32) 10L),                        //passing 10
         ((EIF_INTEGER_32) 11L),                        //passing 11
         tp2);                                          //passing "hello"

Wrapper functions in final mode

Due to the potential optimization described above, in final mode two C wrapper functions are generated:

  • one which expects the closed arguments as a tuple (encapsulated). The only difference to the frozen wrapper is how the agent callee is calculated.
  • and one that expects them as separate parameters.

For the agent creation:

agent c.f (?, 1, ?)

the following wrapper functions are generated:

EIF_REFERENCE _fAaatpmf_2_4 (
   EIF_REFERENCE (*f_ptr)(EIF_REFERENCE, 
                          EIF_INTEGER_32, 
                          EIF_INTEGER_32, 
                          EIF_REFERENCE), 
   EIF_TYPED_ELEMENT* closed, 
   EIF_TYPED_ELEMENT* open)
{
    return f_ptr (closed [1].element.rarg, 
                  open [1].element.i32arg, 
                  closed [2].element.i32arg, 
                  open [2].element.rarg);
}
EIF_REFERENCE __fAaatpmf_2_4 (
   EIF_REFERENCE (*f_ptr)(EIF_REFERENCE, 
                          EIF_INTEGER_32, 
                          EIF_INTEGER_32, 
                          EIF_REFERENCE), 
   EIF_TYPED_ELEMENT* closed, 
   EIF_INTEGER_32 op_2, 
   EIF_REFERENCE op_4)
{
    return f_ptr (closed [1].element.rarg, 
                  op_2, 
                  closed [2].element.i32arg, 
                  op_4);
}

The name of the wrapper for the encapsulated closed arguments always starts with one '_', whereas the other one starts with two underlines.

Melted agent creator

Of course it is possible that the agent creation is melted and hence there is no wrapper. For this situation the Eiffel runtime provides two generic wrapper functions:

RT_LNK void rout_obj_call_procedure_dynamic (
    int stype_id, int feature_id, int is_precompiled, int is_basic_type, int is_inline_agent, EIF_TYPED_ELEMENT* closed_args, 
    int closed_count, EIF_TYPED_ELEMENT* open_args, int open_count, EIF_REFERENCE open_map);
 
RT_LNK void rout_obj_call_function_dynamic (
    int stype_id, int feature_id, int is_precompiled, int is_basic_type, int is_inline_agent, EIF_TYPED_ELEMENT* closed_args, 
    int closed_count, EIF_TYPED_ELEMENT* open_args, int open_count, EIF_REFERENCE open_map, void* res);

These functions calculate the agent callee with the static type id and a feature id. Furthermore the reordering of open and closed arguments has to be done at agent call time with help of the open_map.

Strategy for optimizing dotnet Agent Calls

Work in progress! Compared to the Classic version, dotnet Agent Calls are slow. The a short look at feature apply of class FUNCTION shows why:

apply 
        -- Call function with `operands' as last set.
    do
        if attached {RESULT_TYPE} rout_disp.invoke (target_object, internal_operands) as al_last_result then
            last_result := al_last_result
        end
    end

The actual call is done through reflection. Additionally, in dotnet the two major optimizations are missing:

  • Directly passing the elements of manifest tuples as parameters. (And hence not creating the tuple)
  • Computing the reordering of closed and open operands at compile time. (By generating the proper wrapper)

All these three issues can be removed.

For every agent creation we create:

  • A delegate type that requires two Tuples (closed and open arguments) as arguments.
  • A static method that conforms to this delegate type. This method does the ordering of closed and open arguments and calls the agent callee.
  • A second delegate type, that requires one Tuple (closed arguments) and all the open arguments as arguments.
  • A second static method which conforms to the second delegate type and does the ordering and agent callee call.