CodeQL 文档

查询性能故障排除

通过遵循一些简单的准则,提高您的 CodeQL 查询的性能。

关于查询性能

本主题提供了一些关于如何避免可能影响查询性能的常见问题的简单技巧。在阅读以下技巧之前,值得重申有关 CodeQL 和 QL 语言的一些重要要点。

  • CodeQL 谓词 被评估为数据库 。大型谓词会生成具有许多行的巨大表,因此计算成本很高。
  • QL 语言使用标准数据库操作和 关系代数(例如联接、投影和并集)来实现。有关查询语言和数据库的更多信息,请参阅“关于 QL 语言。”
  • 查询从底向上评估,这意味着只有在所有它依赖的谓词都评估后,才会评估一个谓词。有关查询评估的更多信息,请参阅“QL 程序的评估。”

性能技巧

遵循以下准则,以确保您不会被最常见的 CodeQL 性能陷阱所困扰。

消除笛卡尔积

谓词的性能通常可以通过考虑它大致有多少结果来判断。创建性能不佳的谓词的一种方法是使用两个变量,而不以任何方式关联它们,或者只使用否定关联它们。这会导致计算每个变量可能值的集合之间的笛卡尔积,可能生成一个巨大的结果表。如果您没有对变量指定限制,就会发生这种情况。例如,考虑以下谓词,它检查 Java 方法 m 是否可以访问字段 f

predicate mayAccess(Method m, Field f) {
  f.getAnAccess().getEnclosingCallable() = m
  or
  not exists(m.getBody())
}

如果 m 包含对 f 的访问,则谓词成立,但也保守地假设没有主体的方法(例如,本机方法)可以访问任何字段。

但是,如果 m 是一个本机方法,则由 mayAccess 计算的表将包含一个行 m, f,用于代码库中的所有字段 f,使其可能非常大。

此示例显示了成员谓词中的类似错误

class Foo extends Class {
  ...
  // BAD! Does not use ‘this’
  Method getToString() {
    result.getName() = "ToString"
  }
  ...
}

请注意,虽然 getToString() 未声明任何参数,但它有两个隐式参数,resultthis,它未能关联它们。因此,由 getToString() 计算的表包含 resultthis 的每种组合的行。也就是说,对于名为 "ToString" 的方法和 Foo 实例的每种组合都有一行。为了避免这种错误,this 应该在类 Foo 上的成员谓词 getToString() 中受到限制。

使用特定类型

类型”提供关系大小的上限。这有助于查询优化器更加有效,因此通常最好使用尽可能特定的类型。例如

predicate foo(LoggingCall e)

优先于

predicate foo(Expr e)

从类型上下文中,查询优化器推断出程序的某些部分是冗余的,并将其删除或专门化

确定变量的最特定类型

如果您不熟悉查询中使用的库,可以使用 CodeQL 确定实体的类型。有一个名为 getAQlClass() 的谓词,它返回其被调用的实体的最具体 QL 类型。

例如,如果您正在使用 Java 数据库,您可能会在可调用对象名为 c 的每个 Expr 上使用 getAQlClass()

import java

from Expr e, Callable c
where
    c.getDeclaringType().hasQualifiedName("my.namespace.name", "MyClass")
    and c.getName() = "c"
    and e.getEnclosingCallable() = c
select e, e.getAQlClass()

此查询的结果是该函数中每个 Expr 的最特定类型的列表。您将看到表达式由多个类型表示的多个结果,因此它可能会返回一个非常大的结果表。

使用 getAQlClass() 作为调试工具,但不要将其包含在查询的最终版本中,因为它会降低性能。

避免复杂的递归

递归” 是关于自引用定义。只要使用得当,它就非常强大。总的来说,您应该尽量使递归谓词尽可能简单。也就是说,您应该定义一个基本情况,让谓词能够触底,以及一个递归调用

int depth(Stmt s) {
  exists(Callable c | c.getBody() = s | result = 0) // base case
  or
  result = depth(s.getParent()) + 1 // recursive call
}

注意

查询优化器具有专门的数据结构来处理传递闭包。如果可能,请在简单递归谓词上使用传递闭包,因为它很可能被计算得更快。

折叠谓词

有时您可以通过将大型谓词的一部分“折叠”成较小的谓词来帮助查询优化器。

一般原则是将工作块分离出来,这些工作块是

  • 线性,因此不会有太多分支。
  • 紧密绑定,以便这些块尽可能多地在变量上相互联接。

在以下示例中,我们探讨了对两个 Element 的一些查找

predicate similar(Element e1, Element e2) {
  e1.getName() = e2.getName() and
  e1.getFile() = e2.getFile() and
  e1.getLocation().getStartLine() = e2.getLocation().getStartLine()
}

Element -> FileElement -> Location -> StartLine 是线性的,即每个 Element 只有一个 FileLocation 等。

但是,如上所述,优化器很难选择最佳排序。首先联接,然后执行线性查找可能会导致性能低下。通常,我们希望首先执行快速、线性的部分,然后联接结果更大的表。我们可以通过以下方式拆分上述谓词来启动这种排序

predicate locInfo(Element e, string name, File f, int startLine) {
  name = e.getName() and
  f = e.getFile() and
  startLine = e.getLocation().getStartLine()
}

predicate sameLoc(Element e1, Element e2) {
  exists(string name, File f, int startLine |
    locInfo(e1, name, f, startLine) and
    locInfo(e2, name, f, startLine)
  )
}

现在我们想要的结果结构更清晰了。我们将简单部分分离到自己的谓词 locInfo 中,而主要谓词 sameLoc 只是一个更大的联接。

进一步阅读

  • ©GitHub, Inc.
  • 条款
  • 隐私