Contenido principal

Identify Bottlenecks in User-Defined Coding Standards

R2026b

When a user-defined coding standard takes too long to analyze your codebase, the cause is usually a small number of predicates that produce very large intermediate tables or perform expensive operations. The polyspace-query-language profile command runs a Polyspace® Bug Finder™ analysis with profiling enabled and reports per-predicate metrics — runtime, table size, and memory usage — so you can pinpoint the bottleneck, rewrite the predicate, and verify the improvement.

To use profiling effectively, you need a basic understanding of how PQL executes your rules under the hood. PQL is a declarative language: you describe what to find, not how to find it. Under the hood, PQL compiles to Datalog and runs on the Soufflé engine. Each predicate in a when clause produces an intermediate table of all matching results. When you write Cpp.Function.is(&func), the engine generates a table of every function in the codebase. When you add and func.name(&name), it joins that table with function names. The logical and between predicates is a join — the engine combines rows from both tables. If the tables are large, the join is expensive. The logical or is a union — results from both branches are merged. Because the engine materializes all matching rows with no early exit, a predicate that matches broadly (such as Cpp.Identifier.is, which matches every identifier in the source code) generates a very large table that slows all subsequent operations.

Write a Rule and Profile It

Consider a coding rule: function names must not start with an underscore. A straightforward implementation uses the syntactic class Cpp.Identifier to find all identifiers in the source code, then walks up the syntax tree to check whether each identifier belongs to a function definition:

  • #include <string.h>
    #include <stdlib.h>
    
    /* Functions with leading underscores (violations) */
    
    int _initialize_buffer(char *buf, int size) {
        for (int i = 0; i < size; i++) { buf[i] = 0; }
        return size;
    }
    
    void _process_data(int *data, int len) {
        for (int i = 0; i < len; i++) { data[i] = data[i] * 2 + 1; }
    }
    
    int _calculate_checksum(const char *data, int len) {
        int sum = 0;
        for (int i = 0; i < len; i++) { sum += data[i]; }
        return sum & 0xFF;
    }
    
    char *_format_message(int code, const char *prefix) {
        static char buffer[256];
        int offset = 0;
        while (*prefix && offset < 200) { buffer[offset++] = *prefix++; }
        buffer[offset] = '\0';
        return buffer;
    }
    
    double _compute_average(const int *values, int count) {
        if (count == 0) return 0.0;
        long sum = 0;
        for (int i = 0; i < count; i++) { sum += values[i]; }
        return (double)sum / count;
    }
    
    /* Functions without leading underscores (compliant) */
    
    int initialize_system(void) {
        char buf[128];
        _initialize_buffer(buf, 128);
        return 0;
    }
    
    void run_pipeline(int *data, int len) {
        _process_data(data, len);
        int cs = _calculate_checksum((const char *)data, len * (int)sizeof(int));
        (void)cs;
    }
    
    int validate_input(const char *input) {
        if (input == NULL) return -1;
        int len = (int)strlen(input);
        if (len == 0 || len > 1024) return -1;
        return _calculate_checksum(input, len);
    }
    
    void transform_array(int *arr, int n) {
        for (int i = 0; i < n; i++) { arr[i] = (arr[i] << 1) ^ 0xAB; }
    }
    
    int compare_buffers(const char *a, const char *b, int len) {
        for (int i = 0; i < len; i++) { if (a[i] != b[i]) return i; }
        return -1;
    }
    
    void apply_filter(int *data, int len, int threshold) {
        for (int i = 0; i < len; i++) {
            if (data[i] > threshold) { data[i] = threshold; }
        }
    }
    
    int count_occurrences(const char *str, char target) {
        int count = 0;
        while (*str) { if (*str == target) count++; str++; }
        return count;
    }
    
    void reverse_array(int *arr, int n) {
        for (int i = 0; i < n / 2; i++) {
            int tmp = arr[i]; arr[i] = arr[n - 1 - i]; arr[n - 1 - i] = tmp;
        }
    }
    
    int find_maximum(const int *arr, int n) {
        int max = arr[0];
        for (int i = 1; i < n; i++) { if (arr[i] > max) max = arr[i]; }
        return max;
    }
    
    void fill_pattern(char *buf, int size, const char *pattern) {
        int plen = (int)strlen(pattern);
        for (int i = 0; i < size - 1; i++) { buf[i] = pattern[i % plen]; }
        buf[size - 1] = '\0';
    }
    
    typedef struct { int id; char name[64]; double value; int flags; } Record;
    typedef struct { Record *items; int count; int capacity; } RecordList;
    typedef struct { char key[32]; char value[128]; } ConfigEntry;
    typedef struct { ConfigEntry entries[64]; int num_entries; } Config;
    
    Record create_record(int id, const char *name, double value) {
        Record r; r.id = id;
        strncpy(r.name, name, 63); r.name[63] = '\0';
        r.value = value; r.flags = 0; return r;
    }
    
    int add_record(RecordList *list, Record r) {
        if (list->count >= list->capacity) return -1;
        list->items[list->count++] = r; return 0;
    }
    
    Record *find_record(RecordList *list, int id) {
        for (int i = 0; i < list->count; i++) {
            if (list->items[i].id == id) return &list->items[i];
        }
        return NULL;
    }
    
    int main(void) {
        int data[] = {10, 20, 30, 40, 50, 60, 70, 80};
        int n = sizeof(data) / sizeof(data[0]);
        initialize_system();
        run_pipeline(data, n);
        transform_array(data, n);
        apply_filter(data, n, 100);
        reverse_array(data, n);
        int max = find_maximum(data, n); (void)max;
        char buf[256]; fill_pattern(buf, 256, "ABCD");
        int occ = count_occurrences(buf, 'B'); (void)occ;
        int valid = validate_input("test_input"); (void)valid;
        double avg = _compute_average(data, n); (void)avg;
        char *msg = _format_message(42, "STATUS"); (void)msg;
        Record r = create_record(1, "sample", 3.14); (void)r;
        return 0;
    }
  • package main
    
    #[Categories("Must")]
    catalog profiling_example = {
        section NamingConventions = {
            #[Id(LeadingUnderscoreCheck)
             Description("Function names must not start with an underscore")
             Category("Must")]
            rule LeadingUnderscoreCheck = {
                naming.FuncNameLeadingUnderscore,
            },
        },
    }
  • package naming
    
    defect FuncNameLeadingUnderscore =
    when
        Cpp.Identifier.is(&id)
        and id.toNode(&node)
        and node.getAnAncestor(&anc)
        and Cpp.FunctionDefinition.isa(anc)
        and Cpp.FunctionDefinition.cast(anc, &funcDef)
        and funcDef.declarator(&declNode)
        and Cpp.FunctionDeclarator.cast(declNode, &funcDecl)
        and funcDecl.declarator(&nameNode)
        and nameNode == node
        and id.nodeText(&name)
        and name.regexSearch("^_")
    raise "Function name '{name}' starts with an underscore"
    on id

The rule works correctly — it finds all functions whose names start with an underscore. To check how it performs, package the project and run the profiler:

polyspace-query-language package
polyspace-query-language profile -sources test_profiling.c

You can append any Polyspace Bug Finder option to the profile command.

Note

The profile command runs a full Polyspace Bug Finder analysis with profiling enabled. Use source files that are representative of your production codebase. On small files, differences between implementations might not be visible.

Interpret the Profiling Results

The profiler prints three ranked tables to the console. Each table lists the top predicates by one metric:

  • Runtime (ms) — Time the engine spent computing the predicate's result table.

  • Number of Entries (tuples) — How many rows the predicate's result table contains. A large number means the predicate matches broadly.

  • RSS (kB) — Memory consumed to store the intermediate table.

The profiler also generates an HTML report at .polyspace/.profile/profile_report.html with sortable columns and per-predicate source mapping.

For the rule above, on a C file with approximately 20 functions and 200 lines of code, the profiling results show:

  • Runtime — The defect naming.FuncNameLeadingUnderscore runs in 0.95 ms.

  • Number of Entries — The Cpp.Identifier table contains 290 tuples. This means the engine generated a table with every identifier in the file — variable names, type names, parameters, labels, and function names — even though only 20 of those identifiers are function names.

The Cpp.Identifier table is the bottleneck. It is the largest table in the profiling results, and the defect must walk up the syntax tree from each of the 290 identifiers to check whether it belongs to a function definition. On a large codebase where the identifier table can contain tens of thousands of entries, this cost grows proportionally.

Rewrite the Rule and Re-Profile

The profiling results show that the rule is slow because Cpp.Identifier matches far more entities than necessary. Instead of iterating over all identifiers and filtering down to functions, use the semantic class Cpp.Function, which directly targets function declarations. Replace naming.pql with:

package naming

defect FuncNameLeadingUnderscore =
when
    Cpp.Function.is(&func)
    and func.name(&name)
    and name.regexSearch("^_")
raise "Function name '{name}' starts with an underscore"
on func

This rewritten defect finds the same violations. The difference is in the entry point: Cpp.Function.is generates a table of only function declarations, not every identifier. Re-package and re-profile:

polyspace-query-language package
polyspace-query-language profile -sources test_profiling.c

The profiling results for the rewritten rule show:

  • Runtime — The defect naming.FuncNameLeadingUnderscore now runs in 0.14 ms, nearly 7× faster than before.

  • Number of Entries — The Cpp.Function table contains 20 tuples instead of the 290 tuples from Cpp.Identifier. The engine processes only the entities it needs.

The improvement comes entirely from choosing a more specific entry point. On a large codebase, where the ratio of identifiers to functions is even larger, the speedup is more pronounced.

Optimization Strategies

The rewrite above illustrates the most impactful optimization: replacing a broad syntactic class with a targeted semantic class. Other strategies include:

  • Use a semantic class instead of a syntactic class. Semantic classes such as Cpp.Function, Cpp.Variable, and Cpp.Macro operate on resolved entities whose tables are much smaller than the corresponding syntactic classes. See Choose Between Semantic and Syntactic Classes.

  • Filter early. Place restrictive predicates before expensive ones. For example, filter by type (type.isIntegral()) before performing string operations on the name.

  • Keep predicates short. Each predicate compiles to nested joins. Shorter predicates mean fewer joins and smaller intermediate tables.

  • Mark shared predicates with #[NoInline]. By default, PQL inlines predicates at each call site. A predicate used in multiple rules or or branches is recomputed independently in each location. The #[NoInline] annotation causes the engine to compute the predicate once and reuse the result.

After each change, re-run polyspace-query-language profile and compare the metrics to confirm the improvement.

See Also

Topics