跳转至

IDL 关键字转发的隐患

原文链接: https://www.nv5geospatialsoftware.com/Learn/Blogs/Blog-Details/idl-keyword-forwarding-perils

9435 Rate this article:

No rating

IDL 关键字转发的隐患

Anonym Friday, June 24, 2016

当你向一个例程添加关键字,并希望将它们转发到你调用的另一个例程时,必须非常小心。当关键字总是存在时,这没有问题;但如果它们是可选的输出关键字,你最终可能会复制最外层例程的调用者从未见过的数据。下面是一个简短的例子:

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

当我调用 layer1 时不带任何关键字,我们可以看到 FOO 和 BAR 在 layer1 中都不存在,但在 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

这是因为解释器只查看 layer1 调用 layer2 的方式,并且必须假设 layer1 将 FOO 和 BAR 用作输出关键字。虽然解释器确实有可能检查 foo 和 bar 变量的使用方式,并在意识到这些变量没有在 layer1 的其他地方使用,也没有传入它时尝试优化掉它们,但这会对性能产生重大影响。编译型语言可以通过优化链接器做到这一点,但像 IDL 这样的解释型语言,在每次运行一行代码时都要花时间做这件事,这个代价就太高了。

解释 N_ELEMENTS()ARG_PRESENT() 返回值的一种方式如下表所示:

N_ELEMENTS() EQ 0 N_ELEMENTS() GT 0
ARG_PRESENT() EQ 0 未使用关键字 输入关键字
ARG_PRESENT() EQ 1 输出关键字 输入/输出关键字

如果我设置 FOO 为一个字面量、BAR 为一个未定义变量来调用 layer1,会看到不同的输出:

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

这里我们看到 FOO 是 layer1 的输入,BAR 是输出。更有趣的部分在 layer2,FOO 是输入/输出关键字,而 BAR 仍然只是输出关键字。当我为其中一个关键字使用已定义的变量调用 layer1 时,情况发生了一点变化:

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

这次 FOO 是 layer1 的输入和 layer2 的输入/输出,但 BAR 对 layer1 和 layer2 来说都是输入/输出关键字。

现在你可能想知道这整个分析的意义是什么。当输出关键字需要大量时间构建或大量空间存储,并且被错误地识别为存在时,问题就来了。想象一下 BAR 占用千兆字节的内存,如果用户调用 layer1 时没有指定 BAR,那么 layer2 将分配该内存并将其返回给 layer1,但当 layer1 返回到调用者时,这些内存会被丢弃。

我们如何防御性地实现代码以防止这种时间和空间的浪费?不幸的是,我想出的最佳解决方案是在 layer1 中添加一些逻辑,有条件地将关键字转发给 layer2。这里我们讨论的关键字类型很重要:输入、输出还是输入/输出。纯输入关键字可以盲目转发,而输出和输入/输出关键字则需要额外的逻辑。

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

正如你所料,这种方法最大的缺点是,它随关键字数量呈指数增长,很快就会变得难以控制。我们需要一个 O(n) 而非 O(2^n) 的解决方案,我想出了两种完全不同的方案,各有优缺点。首先是更简单的方案,它使用 EXECUTE () 函数:

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

这种方法构建一个要执行的命令字符串,以它调用的 layer2 例程名称开头。然后,它根据关键字的类型,有条件地使用 N_ELEMENTS() 和/或 ARG_PRESENT() 将关键字附加到命令字符串。这并没有增加很多新代码,但因为 EXECUTE () 必须编译命令字符串然后执行它,所以会带来性能损失。有人可能会指出使用 CALL_PROCEDURE() 效率更高,但它不支持输出和输入/输出关键字,因此我们必须使用 EXECUTE()。

另一种方法避免了 EXECUTE() 的性能损失,但它更复杂一些,需要创建一个新的包装函数并使用 SCOPE_VARFETCH(),而有些人对使用这个函数心存疑虑。

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

这个版本的 layer1 有条件地将关键字添加到一个 Hash 中,该 Hash 在调用新的包装函数时被转换为一个结构体。通过在调用时将该结构体分配给 _EXTRA 关键字,关键字被正确映射。你会注意到,放入 Hash 的变量取决于它是输入关键字还是输出关键字。对于输入关键字,我直接使用了在 layer1 签名中分配给那些关键字的变量,但对于输出关键字,我不得不创建一个局部变量并将其放入 Hash 中。原因是 IDL 结构体不能包含 !NULL 或未定义的值,所以我必须给它们赋一个值。在这种情况下,我使用了 0,但根据 layer2 的预期,可能需要使用一个特殊的“未设置”值。

新的 layer2Wrapper() 函数值得更仔细地研究。它使用 _REF_EXTRA 声明,以便我们获得按引用传递的语义,而不是按值传递。那个 _REF_EXTRA 包只是通过 _EXTRA 关键字(正如我们应该做的那样)传递给 layer2。调用 layer2 之后,包装器然后使用带有 /REF_EXTRA 关键字的 SCOPE_VARFETCH() 来获取每个关键字的值,并将其放入一个 Hash 中,返回给其调用者。正是这个 SCOPE_VARFETCH() 技巧使得创建这个包装例程成为必要。一旦 layer2Wrapper 将 Hash 返回给 layer1,我们就会将 Hash 中的任何现有值复制到适当的输出关键字变量中。

这两种解决方案对于一个看似微小的不便来说可能显得工作量很大,但在实际案例中,我们可能会在那些仅存在于调用栈部分环节且从未由原始调用者指定的关键字上浪费大量时间和/或内存。ENVIRaster::GetData() 方法就是这样一个主要例子。该方法总是返回一个像素值数组,但有一个可选的 PIXEL_STATE 关键字,可用于检索一个相同维度的字节数组,指示哪些像素是好的,哪些是坏的。如果不使用我提出的类似解决方案,无论用户调用 GetData() 时是否指定了该关键字,我们每次都会分配和计算 PIXEL_STATE 数组。

Crop Counting on Mars!? Brute Force PNG Image Sizing