Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

are hidden globals bad programming practice?

I'm writing some linear algebra code (in Fortran 2003, but it would be the same issue in Fortran 90 or C) which requires a few work vectors to do computations in. My idea for dealing with this is to make a work array w(:,:) which is private to the linear algebra module, i.e. a "hidden global" as defined in this discussion of why true global variables are awful.

I imagine this as having a big problem to solve on a blackboard, and for each part of the problem you pick an area of the blackboard to solve it on.

In keeping with that analogy, I could also have a bunch of small whiteboards: define a work_array data type and pass them to the solvers as need be. (PETSc effectively uses this approach through another layer of abstraction; a solver is a data type which includes some procedure pointers to the methods used as well as a few work vectors.) When there are nested calls from one solver to the other, this gets a mite complicated, so I like the first way better. It also doesn't require as much misdirection.

Any thoughts on which approach makes for better programming practice?

EDIT: I also don't think it'll be a problem when I start using OpenMP, which I've already done in an old incarnation of this code. Each thread only accesses its portion of the unknowns and not those of other threads after the problem is set up. Nonetheless, concurrency issues are probably a good reason not to use static variables generally.

If I have to keep dynamically allocating space for the scratch arrays every time I call a solver, which is often, won't that incur a lot of overhead?

like image 990
Daniel Shapero Avatar asked Mar 26 '13 00:03

Daniel Shapero


3 Answers

If you're doing any non-trivial computation in the working space, the cost of malloc and free will be dominated by the cost of the computations performed in the allocated space, and the overhead will be approximately zero. The only time that avoiding allocation makes sense as an optimization strategy is when the amount of work performed on the buffer is so small that the time to obtain the buffer might dominate (or at least, might fail to be dominated by another term). The main situation where this can happen is building strings.

On the other hand, global state has a lot of costs:

  1. It precludes multi-threaded use.
  2. If the state needs to persists between multiple calls, it precludes library use (the library can't be used by more than one part of a program at a time since they might clobber each other's work).
  3. It precludes recursive/re-entrant use.
  4. It uses memory whenever the library is linked, even if the functions are never called.
  5. Even if you make efforts to work around some of these issues, it's a serious code smell and thus a cost in human time, i.e. it will waste the time of the next person who reads your code while they try to determine whether the global state is actually introducing any bugs or usage restrictions on the code.
like image 101
R.. GitHub STOP HELPING ICE Avatar answered Oct 14 '22 17:10

R.. GitHub STOP HELPING ICE


The biggest danger of "hidden globals" (in C's world they are called static) comes when you write concurrent programs. Once you venture into multithreading, having a single blackboard is no longer sufficient: each thread needs its own one. For situations like that, dynamic allocation is more appropriate. If you do not worry about multithreading, having a module-scoped "hidden global" variable is perfectly fine.

like image 22
Sergey Kalinichenko Avatar answered Oct 14 '22 18:10

Sergey Kalinichenko


As for the allocation cost: You can have a derived type containing all the work arrays (in your case the array w(:,:)). You can have one initialization call, which allocates them to the correct size, and then pass the derived type with the allocated arrays in it to the solver as often as possible, something in the following spirit:

module test
   implicit none

    type :: buffer
        integer, allocatable :: work(:,:)
    end buffer

contains

    subroutine init(mybuffer, whatever_else_you_need_for_determinig_allocation_size)
        type(buffer), intent(out) :: mybuffer

         allocate(mybuffer%work(dim1, dim2))
     end subroutine init

     subroutine solver(mybuffer, whatever_else_you_need_for_the_solver)
         type(buffer), intent(inout) :: mybuffer

          ! You can access mybuffer%work here as it is allocated

      end subroutine solver

end module test

But as it had been already pointed out, the allocation cost will be usually negligible with respect to the cost you spend in your solver.

like image 41
Bálint Aradi Avatar answered Oct 14 '22 18:10

Bálint Aradi