X
7826 Rate this article:
No rating

IDL Keyword Forwarding Perils

Anonym

You have to be careful when you add keywords to a routine because you want to forward them on to another routine that you invoke. It’s okay when the keywords are always present, but if they are optional output keywords, you may end up copying around data that the caller of the outermost routine never sees. Here is a short example of this:

pro layer2, FOO=foo, BAR=bar
  compile_opt idl2
 
  print, 'Layer2, N_ELEMENTS(foo) = ' + StrTrim(N_ELEMENTS(foo),2)
  print, 'Layer2, N_ELEMENTS(bar) = ' + StrTrim(N_ELEMENTS(bar),2)
  print, 'Layer2, ARG_PRESENT(foo) = ' + StrTrim(ARG_PRESENT(foo),2)
  print, 'Layer2, ARG_PRESENT(bar) = ' + StrTrim(ARG_PRESENT(bar),2)
end
 
pro layer1, FOO=foo, BAR=bar
  compile_opt idl2
 
  print, 'Layer1, N_ELEMENTS(foo) = ' + StrTrim(N_ELEMENTS(foo),2)
  print, 'Layer1, N_ELEMENTS(bar) = ' + StrTrim(N_ELEMENTS(bar),2)
  print, 'Layer1, ARG_PRESENT(foo) = ' + StrTrim(ARG_PRESENT(foo),2)
  print, 'Layer1, ARG_PRESENT(bar) = ' + StrTrim(ARG_PRESENT(bar),2)
 
  layer2, FOO=foo, BAR=bar
end

When I call layer1 with no keywords, we see how both FOO and BAR are not present in layer1, but they are in layer2:

IDL> layer1
Layer1, N_ELEMENTS(foo) = 0
Layer1, N_ELEMENTS(bar) = 0
Layer1, ARG_PRESENT(foo) = 0
Layer1, ARG_PRESENT(bar) = 0
Layer2, N_ELEMENTS(foo) = 0
Layer2, N_ELEMENTS(bar) = 0
Layer2, ARG_PRESENT(foo) = 1
Layer2, ARG_PRESENT(bar) = 1

This is because the interpreter only looks at the way layer1 invokes layer2, and has to assume that layer1 is using FOO and BAR as output keywords. While it is certainly possible that the interpreter could inspect how the foo and bar variables are used, and try to optimize them away when it realizes that these variables aren’t used elsewhere inside layer1 and weren’t passed into it, that would have a major impact on performance. Compiled languages can do this with an optimizing linker, but interpreted languages like IDL can afford to take the time to do that every time they run a line of code.

One way to interpret the values of N_ELEMENTS() and ARG_PRESENT() is shown in this table:

  N_ELEMENTS() EQ 0 N_ELEMENTS() GT 0
ARG_PRESENT() EQ 0 Keyword not used Iput keyword
ARG_PRESENT() EQ 1 Output keyword Int/out keyword

 

If I call layer1 with FOO and BAR set to a literal and an undefined variable, we see different output:

 

IDL> layer1, FOO=1, BAR=b
Layer1, N_ELEMENTS(foo) = 1
Layer1, N_ELEMENTS(bar) = 0
Layer1, ARG_PRESENT(foo) = 0
Layer1, ARG_PRESENT(bar) = 1
Layer2, N_ELEMENTS(foo) = 1
Layer2, N_ELEMENTS(bar) = 0
Layer2, ARG_PRESENT(foo) = 1
Layer2, ARG_PRESENT(bar) = 1

Here we see how FOO is an input to layer1, and BAR is an output. The more interesting part is in layer2, where FOO in an in/out keyword while BAR is still only an output keyword. The situation changes a little when I call layer1 with a defined variable for one of the keywords:

IDL> b2 = 2
IDL> layer1, FOO=1, BAR=b2
Layer1, N_ELEMENTS(foo) = 1
Layer1, N_ELEMENTS(bar) = 1
Layer1, ARG_PRESENT(foo) = 0
Layer1, ARG_PRESENT(bar) = 1
Layer2, N_ELEMENTS(foo) = 1
Layer2, N_ELEMENTS(bar) = 1
Layer2, ARG_PRESENT(foo) = 1
Layer2, ARG_PRESENT(bar) = 1

This time FOO is an input to layer1 and in/out to layer2, but BAR is an in/out keyword to both layer1 and layer2.

Now you may be wondering what the point of this whole analysis is. It matters when an output keyword takes a lot of time to build or space to store, and is incorrectly identified as being present. Imagine that BAR uses a gigabyte of memory, if the user calls layer1 without BAR, then layer2 will allocate that memory and return it to layer1, but it gets thrown away when layer1 returns to the caller.

How do we defensively implement the code to prevent this waste of time and space? Unfortunately the best solutions I’ve come up with are to add some logic to layer1 to conditionally forward keywords to layer2. It matters what type of keyword we are talking about here, input vs output vs in/out. Pure input keywords can be blindly forwarded, while output and in/out need the extra logic.

pro layer2, INPUT=input, OUTPUT=output, INOUT=inout
  compile_opt idl2
 
  print, 'Layer2, N_ELEMENTS(input) = ' + StrTrim(N_ELEMENTS(input),2)
  print, 'Layer2, N_ELEMENTS(output) = ' + StrTrim(N_ELEMENTS(output),2)
  print, 'Layer2, N_ELEMENTS(inout) = ' + StrTrim(N_ELEMENTS(inout),2)
  print, 'Layer2, ARG_PRESENT(input) = ' + StrTrim(ARG_PRESENT(input),2)
  print, 'Layer2, ARG_PRESENT(output) = ' + StrTrim(ARG_PRESENT(output),2)
  print, 'Layer2, ARG_PRESENT(inout) = ' + StrTrim(ARG_PRESENT(inout),2)
 
  output = 'output'
  inout = ISA(inout) ? inout+1 : 'new inout'
end
 
pro layer1, INPUT=input, OUTPUT=output, INOUT=inout
  compile_opt idl2
 
  print, 'Layer1, N_ELEMENTS(input) = ' + StrTrim(N_ELEMENTS(input),2)
  print, 'Layer1, N_ELEMENTS(output) = ' + StrTrim(N_ELEMENTS(output),2)
  print, 'Layer1, N_ELEMENTS(inout) = ' + StrTrim(N_ELEMENTS(inout),2)
  print, 'Layer1, ARG_PRESENT(input) = ' + StrTrim(ARG_PRESENT(input),2)
  print, 'Layer1, ARG_PRESENT(output) = ' + StrTrim(ARG_PRESENT(output),2)
  print, 'Layer1, ARG_PRESENT(inout) = ' + StrTrim(ARG_PRESENT(inout),2)
 
  if (ARG_PRESENT(output)) then begin
    if (ARG_PRESENT(inout)) then begin
      layer2, INPUT=input, OUTPUT=output, INOUT=inout
    endif else begin
      layer2, INPUT=input, OUTPUT=output
    endelse
  endif else begin
    if (ARG_PRESENT(inout)) then begin
      layer2, INPUT=input, INOUT=inout
    endif else begin
      layer2, INPUT=input
    endelse
  endelse
end

The big drawback with this approach, as you can expect, is that it is exponential in the number of keywords and quickly becomes untenable. We need a solution that is O(n) instead of O(2n), and I’ve come up with two completely different solutions which each have their pros and cons. First the simpler solution, which uses the EXECUTE () function:

pro layer1, INPUT=input, OUTPUT=output, INOUT=inout
  compile_opt idl2
 
  print, 'Layer1, N_ELEMENTS(input) = ' + StrTrim(N_ELEMENTS(input),2)
  print, 'Layer1, N_ELEMENTS(output) = ' + StrTrim(N_ELEMENTS(output),2)
  print, 'Layer1, N_ELEMENTS(inout) = ' + StrTrim(N_ELEMENTS(inout),2)
  print, 'Layer1, ARG_PRESENT(input) = ' + StrTrim(ARG_PRESENT(input),2)
  print, 'Layer1, ARG_PRESENT(output) = ' + StrTrim(ARG_PRESENT(output),2)
  print, 'Layer1, ARG_PRESENT(inout) = ' + StrTrim(ARG_PRESENT(inout),2)
 
  cmd = 'layer2'
 
  if (N_ELEMENTS(input)) then begin
    cmd += ', INPUT=input'
  endif
  if (ARG_PRESENT(output)) then begin
    cmd += ', OUTPUT=output'
  endif
  if (N_ELEMENTS(inout) || (ARG_PRESENT(inout)) then begin
    cmd += ', INOUT=inout'
  endif
 
  !null = EXECUTE(cmd)
end

This approach builds up a command string to execute, starting with the name of the layer2 routine it invokes. It then conditionally appends the keywords to the command string, using N_ELEMENTS() and/or ARG_PRESENT(), depending on what type of keyword it is. This doesn’t add a lot of new code, but it takes a performance hit because EXECUTE () has to compile the command string and then execute it. One might point out that using CALL_PROCEDURE() is more efficient, but it doesn’t support output and in/out keywords so we have to use EXECUTE().

The other approach avoids the performance hit of EXECUTE(), but it is a bit more involved, requiring the creation of a new wrapper function and the use of SCOPE_VARFETCH(), which some people are leery to use.

function layer2Wrapper, _REF_EXTRA=extra
  compile_opt idl2
 
  layer2, _EXTRA=extra
 
  if (N_ELEMENTS(extra) eq 0) then return, Hash()
 
  retVal = Hash()
  foreach key, extra do begin
    retVal[key] = SCOPE_VARFETCH(key, /REF_EXTRA)
  endforeach
  return, retVal
end
 
pro layer1, INPUT=input, OUTPUT=output, INOUT=inout
  compile_opt idl2
 
  print, 'Layer1, N_ELEMENTS(input) = ' + StrTrim(N_ELEMENTS(input),2)
  print, 'Layer1, N_ELEMENTS(output) = ' + StrTrim(N_ELEMENTS(output),2)
  print, 'Layer1, N_ELEMENTS(inout) = ' + StrTrim(N_ELEMENTS(inout),2)
  print, 'Layer1, ARG_PRESENT(input) = ' + StrTrim(ARG_PRESENT(input),2)
  print, 'Layer1, ARG_PRESENT(output) = ' + StrTrim(ARG_PRESENT(output),2)
  print, 'Layer1, ARG_PRESENT(inout) = ' + StrTrim(ARG_PRESENT(inout),2)
 
  keywords = Hash()
 
  if (N_ELEMENTS(input)) then begin
    keywords['input'] = input
  endif
  if (ARG_PRESENT(output)) then begin
    myOutput = 0
    keywords['output'] = myOutput
  endif
  if (N_ELEMENTS(inout)) then begin
    keywords['inout'] = inout
  endif else if (ARG_PRESENT(inout)) then begin
    myInout = 0
    keywords['inout'] = myInout
  endif
 
  outKeys = layer2Wrapper(_EXTRA=keywords.ToStruct())
  if (outKeys.HasKey('OUTPUT')) then output = outKeys['OUTPUT']
  if (outKeys.HasKey('INOUT')) then inout = outKeys['INOUT']
end

This version of layer1 conditionally adds the keywords to a Hash, which is converted to a struct when calling the new wrapper function. By assigning the struct to the _EXTRA keyword during invocation, the keywords are properly mapped. One will note that the variables put into the Hash depend on whether it is an input or output keyword. For input keywords, I just used the variables assigned to those keywords in layer1’s signature, but for output keywords I had to create a local variable and put that in the Hash. The reason for this is that IDL structs cannot contain !NULL or undefined values, so I had to assign them a value. In this case I used 0, but depending on the expectations of layer2 a special “not set” value may need to be used.

The new layer2Wrapper() function is worth examining more closely. It is declared using _REF_EXTRA, so that we get pass by reference semantics instead of pass by value. That _REF_EXTRA bag is simply passed along to layer2, using the _EXTRA keyword like we’re supposed to. After calling layer2, the wrapper then uses SCOPE_VARFETCH() with the /REF_EXTRA keyword to grab each keyword’s value and put it into a Hash that is returned to its caller. It’s this SCOPE_VARFETCH() trick that necessitates the creation of this wrapper routine. Once layer2Wrapper returns that Hash to layer1, we then copy any of the existing values out of the Hash into the appropriate output keyword variables.

Both solutions may seem like a lot of work for what seems like a minor inconvenience, but there are real world cases where we could waste a lot of time and/or memory on keywords that only exist for parts of the callstack and are never specified by the original caller. A prime example of this is in ENVIRaster::GetData(). This method always returns an array of pixel values, but there is the optional PIXEL_STATE keyword that can be used to retrieve a byte array of the same dimensions that indicates which pixels are good and which are bad. Without using a solution like the ones I’ve presented, we would end up with the PIXEL_STATE array being allocated and calculated every time a user calls GetData(), whether they specified that keyword or not.