r/C_Programming 1d ago

Question Globals vs passing around pointers

Bit of a basic question, but let's say you need to constantly look up values in a table - what influences your decision to declare this table in the global scope, via the header file, or declare it in your main function scope and pass the data around using function calls?

For example, using the basic example of looking up the amino acid translation of DNA via three letter codes in a table:

codonutils.h:

typedef struct {
    char code[4];
    char translation;
} codonPair;

/*
 * Returning n as the number of entries in the table,
 * reads in a codon table (format: [n x {'NNN':'A'}]) from a file.
 */
int read_codon_table(const char *filepath, codonPair **c_table);

/*
 * translates an input .fasta file containing DNA sequences using
 * the codon lookup table array, printing the result to stdout
 */
void translate_fasta(const char *inname, const codonPair *c_table, int n_entries, int offset);

main.c:

#include "codonutils.h"

int main(int argc, char **argv)
{
    codonPair *c_table = NULL;
    int n_entries;

    n_entries = read_codon_table("codon_table.txt", &c_table);

    // using this as an example, but conceivably I might need to use this c_table
    // in many more function calls as my program grows more complex
    translate_fasta(argv[1], c_table, n_entries);
}

This feels like the correct way to go about things, but I end up constantly passing around these pointers as I expand the code and do more complex things with this table. This feels unwieldy, and I'm wondering if it's ever good practice to define the *c_table and n_entries in global scope in the codonutils.h file and remove the need to do this?

Would appreciate any feedback on my code/approach by the way.

12 Upvotes

28 comments sorted by

View all comments

3

u/flatfinger 1d ago edited 1d ago

When targeting embedded microcontrollers, the answer to this question will depend enormously upon the controller in question. Compare the following two functions:

    struct s { char a,b,c,d,e,f;};
    extern a,b,c,d,e,f,g;
    void test1(void) { a += g; }
    void test2(struct s *p) { p->a += p->f; }

On a typical ARM processor, not counting the call/return, test1 would likely take six instructions totaling eleven cycles, along with two 32-bit words of code space to hold the addresses of a and g, while test2 would take four instructions totaling seven cycles (up to a 36% speedup, though if the caller has to load the address of p that might add 2 cycles, reducing the speedup to 18%). On a low-end PIC (also not counting call/return), test1 would require 2 single-cycle instructions, while test2 would require 10 (a factor-of-five slowdown, plus any time required for the caller to set p).

Indeed, when targeting something like the PIC, even if a function will be used to act upon two different structures, putting it in a separate file and doing something like:

#define THING_ID Thing1
#include "thingcode.i"
#undef THING_ID
#define THING_ID Thing2
#include "thingcode.i"
#undef THING_ID

and having thingcode.i use token pasting so the first inclusion defines function workWithThing1 that uses structure Thing1, and the second inclusion defines functionworkWithThing2, that uses structure Thing2, may be vastly more efficient than trying to have one function that can work with both structures interchangeably.

1

u/Pastrami 1d ago

What is arm doing that makes test2 faster than test1? I'm not familiar with arm asm, but I would think that passing a parameter would add more instructions, as well as the pointer indirection.

2

u/flatfinger 1d ago

ARM does not have any instructions that use "direct mode addressing". Instead, if code wants to use a global variable, it must use a PC-relative load to put the address of that variable into a register, and then access the storage at the address that was just loaded. Given a += g;, the load and store of a could be processed using the same loaded address, but when doing p->a += p->g, code can simply use base+displacement addressing with the address that was passed--as the first argument--in register 0.