BATCH 3 | Project Stage 2, Part 2 - Testing, Errors and Fixes!

 Project Stage 2

Testing, Errors and Fixes!

Hi everyone! This blog post continues from my Project Stage 2, Part 1, where I’ll walk through the challenges I encountered during testing. I will also share my debugging process and do my best to pinpoint the issues along with their respective fixes. Down below is my initial Clone-Prune Analysis Pass which will be follow by some of the challenges I faced:

Initial Clone-Prune Analysis Pass Code: 

include "config.h"

#include "system.h"

#include "coretypes.h"

#include "backend.h"

#include "tree.h"

#include "basic-block.h"

#include "function.h"

#include "gimple.h"

#include "gimple-iterator.h"

#include "tree-pass.h"

#include "context.h"

#include <string.h>


namespace {


const pass_data count_pass_data = {

    GIMPLE_PASS,

    "count",

    OPTGROUP_NONE,

    TV_NONE,

    PROP_gimple_any,

    0, 0, 0, 0

};


static bool seen_clone = false;

static char stored_base[256] = "";

static int stored_bb_count = 0;

static int stored_gimple_count = 0;


class count_pass : public gimple_opt_pass {

public:

    count_pass(gcc::context *ctxt)

      : gimple_opt_pass(count_pass_data, ctxt) {}


    bool gate(function *fun) override {

        (void) fun;

        return true;

    }


    // Execute is called once per function.

    unsigned int execute(function *fun) override {

        // Step 2: Detect cloned functions by examining the function name.

        const char *full_name = function_name(fun);

        char base_name[256] = "";

        bool is_clone = false;

        const char *dot = strchr(full_name, '.');

        if (dot != nullptr) {

            // A dot indicates this function is a clone.

            is_clone = true;

            size_t len = dot - full_name;

            if (len >= sizeof(base_name))

                len = sizeof(base_name) - 1;

            strncpy(base_name, full_name, len);

            base_name[len] = '\0';

            fprintf(stderr, "Detected clone for function: %s (full name: %s)\n",

                    base_name, full_name);

        else {

            // Not a clone. Use the full name as the base name.

            strncpy(base_name, full_name, sizeof(base_name) - 1);

            base_name[sizeof(base_name) - 1] = '\0';

            fprintf(stderr, "Processing original function: %s\n", full_name);

        }


// Count the basic blocks and GIMPLE statements.

        int bb_count = 0, gimple_count = 0;

        basic_block bb;

        FOR_EACH_BB_FN(bb, fun) {

            for (gimple_stmt_iterator gsi = gsi_start_bb(bb);

                 !gsi_end_p(gsi);

                 gsi_next(&gsi)) {

                gimple_count++;

            }

            bb_count++;

        }


// Step 3: Compare cloned functions.

        if (is_clone) {

            if (!seen_clone) {

                seen_clone = true;

                strncpy(stored_base, base_name, sizeof(stored_base) - 1);

                stored_base[sizeof(stored_base) - 1] = '\0';

                stored_bb_count = bb_count;

                stored_gimple_count = gimple_count;

            else {

                // Second clone encountered; compare with stored clone.

                if (strcmp(stored_base, base_name) == 0 &&

                    stored_bb_count == bb_count &&

                    stored_gimple_count == gimple_count) {

                    // They are substantially the same.

                    if (dump_file)

                        fprintf(dump_file, "PRUNE: %s\n", stored_base);

                    else

                        fprintf(stderr, "PRUNE: %s\n", stored_base);

                else {

                    // They differ.

                    if (dump_file)

                        fprintf(dump_file, "NOPRUNE: %s\n", stored_base);

                    else

                        fprintf(stderr, "NOPRUNE: %s\n", stored_base);

                }

                // Reset stored clone info for the next pair.

                seen_clone = false;

            }

}


        // Step 4: Output the function's basic info (always).

        if (dump_file)

            fprintf(dump_file, "Function %s: %d BBs, %d GIMPLE stmts\n",

                    full_name, bb_count, gimple_count);

        else

            fprintf(stderr, "Function %s: %d BBs, %d GIMPLE stmts\n",

                    full_name, bb_count, gimple_count);

        return 0;

    }

};


}


opt_pass *make_count_pass(gcc::context *ctxt) {

    return new count_pass(ctxt);

}



Challenge # 1: Outdated GCC

The first challenge I faced was a simple error that involves using an outdate version of GCC. As you notice, after running the make command, it resulted in an error coming from the clone-test-core.c. 


How I Fixed The Issue:

I then changed the path to the GCC I installed from Lab 4 and after setting the path, I checked the version I used to confirm. 


Result:

My clone-prune analysis pass successfully displayed the following output in the terminal.


I checked the list of files in this directory after using the make command. By using the command ls, I was able to see all the files. To check the diagnostic dump to ensure my clone-prune analysis pass works, I have to look for each of prune and no-pure clone-test dump files.


Since it was difficult to find the files, I used the command ls *count to find the files for no-prune and prune close tests to see if my clone-prune analysis pass works on the test files provided.



Challenge # 2: Incorrect PRUNE Output

Down below are screenshots of clone-test-aarch64-noprune-clone-test-core.c.270t.count and clone-test-aarch64-prune-clone-test-core.c.270t.count. As you notice, both files produced NOPRUNE results even when we expected PRUNE as a result in clone-test-aarch64-prune for the function scale_samples as they are substantially the same.

clone-test-aarch64-prune diagnostic dump:


clone-test-aarch64-noprune diagnostic dump:


Let's investigate!

Given the diagnostic dump files, I immediately realized there was a problem with my clone-prune analysis code logic. To build a clone-prune analysis, it is important that the pass identifies cloned functions. As soon as these functions are identified, we then compare the functions and analyze whether or not they are substantially the same. If so, function must be pruned.

Firstly, I need to see the logic behind identifying cloned functions, and fortunately this part of the code worked as intended. Since it was stated that the cloned functions were named scale_samples, I checked to see if any of PRUNE/NOPRUNE output appears in the dump files. As you notice, it did print out NOPRUNE, which means this part of the code logic performed as expected.


Secondly, I did a walkthrough of the code block for comparing cloned functions to fully understand where the error could be found. By doing this, I then realized that my pass compares functions on the number of basic blocks and GIMPLE statements when it should have considered other aspects of the cloned functions. My pass comparison was made too simple that it failed to catch subtle differences and the entire implementation of the code to determine whether or not they are substantially the same. While the assignment specifies that there will be a resolver function along with the clone variant, my pass was not able to differentiate between the actual clone and the resolver.

Another thing to take not of is that this code failed to identify the differences between the cloned functions, based on their name and that it currently assumes that the first encountered clone and the second encountered clone belong to the same original function. If the functions are processed in an unexpected order, or if the resolver function has differences, my pass would think of this as a discrepancy. Since there is the existence of both resolver and variant functions, I have to skip the resolver function and compare the original code with the variant to determine whether or not they are identical.


// Step 3: Compare cloned functions.

        if (is_clone) {

            if (!seen_clone) {

                seen_clone = true;

                strncpy(stored_base, base_name, sizeof(stored_base) - 1);

                stored_base[sizeof(stored_base) - 1] = '\0';

                stored_bb_count = bb_count;

                stored_gimple_count = gimple_count;

            else {

                // Second clone encountered; compare with stored clone.

                if (strcmp(stored_base, base_name) == 0 &&

                    stored_bb_count == bb_count &&

                    stored_gimple_count == gimple_count) {

                    // They are substantially the same.

                    if (dump_file)

                        fprintf(dump_file, "PRUNE: %s\n", stored_base);

                    else

                        fprintf(stderr, "PRUNE: %s\n", stored_base);

                else {

                    // They differ.

                    if (dump_file)

                        fprintf(dump_file, "NOPRUNE: %s\n", stored_base);

                    else

                        fprintf(stderr, "NOPRUNE: %s\n", stored_base);

                }

                // Reset stored clone info for the next pair.

                seen_clone = false;

            }

}




Based on the code above, although it did do a comparison between the first and second cloned functions, it did not handle the resolver function separately. If the resolver function was not filtered during the comparison, then it will produce a NOPRUNE output despite the functions being identical as they will not be compared as separate cloned functions.

How I Fixed The Issue: 

While keeping the approach for identifying cloned functions by finding (.) and extracting the base name, I have added static variables into the signature of the first encountered clone to make the comparison more precise. When a second clone is detected, the code compares its signature with the first stored one. If they match indicating the clones are substantially identical, then the pass outputs a diagnostic PRUNE message, otherwise it outputs NOPRUNE message. 

The changes I made to the original code includes setting the flag if it isn't a resolver - that way, the original function and the variant will be compared properly, without the resolver being considered. For clone comparison, both the original and the function variant should be considered and since there is a resolver function, this had to be filtered and treated as a separate function -  otherwise, the cloned functions will not be compared as intended. Down below is the updated code logic that checks for resolver to ensure proper resolver function filtering:

   // Determine if this function is a resolver.

        bool is_resolver = (strstr(full_name, "resolver") != nullptr);

        const char *dot = strchr(full_name, '.');

        if (dot != nullptr) {

            size_t len = dot - full_name;

            if (len >= sizeof(base_name))

                len = sizeof(base_name) - 1;

            strncpy(base_name, full_name, len);

            base_name[len] = '\0';

            fprintf(stderr, "Detected clone for function: %s (full name: %s)\n",

                    base_name, full_name);

        } else {

            strncpy(base_name, full_name, sizeof(base_name) - 1);

            base_name[sizeof(base_name) - 1] = '\0';

            fprintf(stderr, "Processing original function (for clone comparison): %s\n", full_name);

        }


        ...

        ...

        ...


        // Only perform clone comparison for non-resolver functions.

        if (!is_resolver) {

            if (!seen_clone) {

                seen_clone = true;

                strncpy(stored_base, base_name, sizeof(stored_base) - 1);

                stored_base[sizeof(stored_base) - 1] = '\0';

                stored_bb_count = bb_count;

                stored_gimple_count = gimple_count;

            } else {

                if (strcmp(stored_base, base_name) == 0 &&

                    stored_bb_count == bb_count &&

                    stored_gimple_count == gimple_count) {

                    // They are substantially the same.

                    if (dump_file)

                        fprintf(dump_file, "PRUNE: %s\n", stored_base);

                    else

                        fprintf(stderr, "PRUNE: %s\n", stored_base);

                } else {

  if (dump_file)

                        fprintf(dump_file, "NOPRUNE: %s\n", stored_base);

                    else

                        fprintf(stderr, "NOPRUNE: %s\n", stored_base);

                }

                seen_clone = false;

            }

        } else {

            fprintf(stderr, "Skipping resolver function: %s\n", full_name);

        }



The screenshots below are the diagnostic output from the clone test dump files. To prove that the screenshots are taken from the respective prune and noprune clone test files, take a look at the name of the function. If it includes "rng" then it is for PRUNE and if it is SVE2, it is taken from NOPRUNE test file.

clone-test-aarch64-prune diagnostic dump:

clone-test-aarch64-noprune diagnostic dump:


End of Part 2

Thank you so much for taking the time to read this blog post. Stay tuned for Part 3 of Project Stage 2 where I do function comparisons and lessons learned throughout the process of getting my clone-prune analysis pass to perform as intended!















Comments

Popular posts from this blog

BATCH 3 | Project Stage 2, Part 3 - Cloned Functions Comparison and Reflection

Lab 01 - Experiments, Calculating Performance and Modifying Code

BATCH 4 | Clone-Prune Analysis Code Pass On Both Architectures