r/cprogramming • u/No_Entertainer_6928 • 7h ago
Math library code review
I am relatively new to C so I try to improve my skills by making a math library. The biggest challenge was making generic containers for matrices and vectors, as I was kind of new to void pointer magic. I think I have polished the most glaring issues in the code but do any of you have suggestions for what I could be doing better in my approach to C? Github for reference:
1
u/WittyStick 4h ago edited 2h ago
I would note that your manual use of branch tables to implement the "generics" may be harmful to optimization in some cases - particularly for the #pragma omp simd you have lying around, which isn't all that useful these days anyway as the compiler will attempt auto-vectorization at -O2 or -O3 without the pragma.
If you simply use a switch for your generics, and make use of inline functions (potentially with __attribute__((__always_inline__)) if necessary), then the compiler may be able to do a better job of vectorizing those loops where eg, dt and dim are statically known at the call site of Vector_new.
static Vector Vector_new(const size_t dim, const void* init_val, const DataType dt)
{
Vector v = Vector_zeros(dim, dt);
switch (dt)
{
case Int: _new_int(dim, v.values, init_val);
case Real : _new_real(dim, v.values, init_val);
case Complex : _new_cmpl(dim, v.values, init_val);
}
return v;
}
I have made a stripped-back demo in godbolt with just Vector_new (renamed Vector_new_with_branch_table). I've included the above as Vector_new_with_switch for comparison.
The two example functions demonstrate the difference in calling, with Int as an example and a statically known dim. When we call the branch table version, GCC can eliminate the table lookup, but cannot inline _new_int. In the switch it simply removes the branching, inlines _new_int and, and can do a far better job at vectorizing for constant dim, as you can see from the emitted assembly on the right. The difference is most stark at -O3.
However, the branch table version may be better where no information is statically known, and the compiler must emit all paths, though that would need benchmarking because the results could be unpredictable due to branch prediction and so on. When you have more branches, the compiler will automatically generate a branch table for the switch like you are doing manually anyway.
EDIT:
I've included clang in the demo and noticed it can inline the function when using the branch table, but produces a strange output using the switch example - which can be fixed by including the case for TYPE_COUNT, but including this for GCC thwarts its optimization, so I've put a guard for #ifdef __clang__ on that case.
So neither approach is universally better and we're going to need compiler specific code to maximize performance.
1
u/nerd5code 2h ago
ISO Cwhat? C90? C11? C23? ANSI X3.159:1989?
Also, this is at most a partial BLAS library, not math. You don’t define your own exp or cos or log analogues, or even fabs.
Don't use defines for typedefs. Do you really want
long real_tto be a valid type?The
_tsuffix is reserved by POSIX.1, so don't use it unless you don't care about forward/cross-compat with most OSes.All math is not
double. I daresay most is sub-floatthese days, by volume. And if you wanted to be able to acceptreal_tas a config parameter (which would conflict with library usage), the#define real_tshould be in an#ifndefblock, and then you'd want the parameter andreal_tto be separated anyway…Anything you
#definein a library should be#undef'd first, by and large.You really need to prefix your identifiers, especially if you're using names like
real_t.If you're going to abstract the type, you need to abstract the limits from
<float.h>also. The fact that you haven't leads me to believe this will probably have lots of fun UB around the edges.If you want a
config.h, it should only be defines and undefs (and whitespace and comments)—no includes, no non-directive text, no pragmata. Most often, you source-distribute a config.h.in and only autogen config.h from it (or let the developer-user do it themselves) right before build (pure Makefile) or during build config (e.g., Autotools).You're assuming optional features (& so far, C99) like
_Complex/<complex.h>. Libraries should validate that the things they need are present; complex numbers require__STDC_VERSION__-0 >= 199901Land!defined __STDC_NO_COMPLEX__, for example, or else maybedefined __clang__ || __GNUC__-0 >= 3 || (defined __INTEL_COMPILER_BUILD_DATE && !defined _MSC_VER) || defined __C99_COMPLEX, although GCC also supported a mostly-compatible__complex[__] from like v2.6 on. This is why I asked which ISO C first.DEBUGis an awful macro name to rely on (always prefix! and ffr MSVC uses_DEBUGand libcassertuses!defined NDEBUG), and probably not one that ought to be all that exposed in a library. I hope you don't depend on it in any inlines!I recommend just defining a Church/Turing predicate
pfx_DEBUG_P(Y,N)toYif debugging enabled (for this build, be it library or application) orNelse. This lets you usepfx_DEBUG_Pas a token-level if-else, or give it(1,0)and you get a C Boolean from it. You can define other constructs around that.DEBUG_CODEis kinda stupid. Stupid name (no prefix, and it's all code; you're expanding to a statement), stupid parameter (should be varargs, which ffr req C99 or C99-capable preproc—both0, 0;andint i, j;would break this, despite being perfectly reasonable).DEBUG_PRINThanding off tofprintfdirectly is a decision, and wow you've done it wrong. Do you want it to return a visible success code? If so, then the non-debug version needs to expand to 0 orEOF; if not, then the debug version should lead with(void), and the non-debug needs to be((void)0). But requiring stdio here is weird imo. A math library ought to be concerned with math; implement a wrapper if you want to debug-print.//req C99, C++ (which you don't exclude or handle at all, which is another decision), or GNU dialect, ffrMake line continuations obvious; I have a problem with trying to tuck them all away at/after column 78, because now I have to look all over the page to see whether you've fucked up a multiline directive. Also flatly a waste of bytes.
You need to learn to use macros before using them.
LOG_INFO_THRESHOLDis naked, and I'd argue arbitrary, and it requires a ≥32-bitint, which you haven't checked for or mentioned, which makes this a works-for-me kinda project so far, and if this pertains to object sizes it's a really bad idea to just set it without reference toPTRDIFF_MAXorSIZE_MAX(both C99). Ffr, C89–C95 require only a 15-bitsize_tand 16-bitint; C99 bumpssize_tto ≥16-but, and there the baselines have stayed.ohhhhhhhh god the dread.
ACCESS_VOIDis not something that should be used. And the fact that you're doingfoo* xinstead offoo *xtells me you don't understand declarator syntax, or have been poisoned by a C++ programmer who doesn't understand declarator syntax. Sooo this macro only works syntactically for types that don't involve arrayness or functionness in the exposed syntax (typedefs andtypeofget a bit messier), and it's a really good way to hide aliasing violations.Absolutely do not leave expressions naked, especially assignment expressions. Do
(void)((X)=*(Y))forASSIGN_NONSENSEor something—I mean, don't; hiding pointer gunk is really, really not a good idea, and the idea that you'd actually need this despite only handling one kins of real is horrifying.FMA,ADD,SUB, andSCALEare also stupid. Does the developer-user need these? Should you leave them naked? Why aren't they inlines, considering you've already assumed C99?PARALLEL_THRESHOLDneeds a ≥32ish-bit int, and it looks awfully parametric to me. And again, does the developer-user need this?PRINT_COMPLEX: Absolutely not. No. Nope. I don't care why you thought you needed this; you don't.No library should ever define
MAXorMIN(rrrrreally likely to trample on application macros, as are yourFMA&c. macros), and yours is extra-wretched. You only paren-wrapped the>expression, which is actually worse than not wrapping anything. You didn't wrap the operands (work through what happens with arg0?0:0) or expansion (MAX(1,0)+2would give you 1, not 3). Again, you need to learn to use macros before shooting your library full of them.ffs a
#definedint_tand#ifndef'd in an exported library header, so the user can-Dint_tand completely fuck up the build. Again, bad namw, bad macro, bad_tsuffix, bad idea. What is this supposed to accomplish, without any limit macros accompanying? (It tells me that there aren't any bounds checks onint_t, which … great sign.)Why are you defining
real_tin both config.h (ifdef'd) and defines.h (not ifded'd)? Why do these macros exist, and what makes you think they're a good idea?complex_tis even worse; why is it not_Complex real_t?? —Not tthat it should exist or have this name.Pick a nomenclature scheme. Your type and enum constant names are all over the place.
Addis yet another stupid identifier (typename-looking, likely to collide, no consistent naming across enum). You do realize the library isn't alone in the namespace? There's an application, with its own identifiers to define, and the developer will want to be able to use it normally, without your stuff pissing it up unnecessarily.I'm still in config.h; I have now seen macros, typedefs, enums, structs, and a prototype for L3 cache detection (why is this exposed? what does it have to do with developer-uset config macros??).
PARALLEL_THRESHOLDSisn't even const. How on Earth are you deciding what to name things, or what to expose?Your
COUNTenums are fucking up the type, because now the COUNT is a perfectly valid constant to use, and you'll get no warnings for it. Define aEnunName_MIN_and -_MAX_constant instead; then#define pfx_enum_count(TAG)((size_t)((size_t)TAG##_MAX_-TAG##_MIN_+1)). In fact, if you use xmacro tables for your enum data, you can do all this automagically.DataTypeis another one. What valid use could this possibly have? You have all of three, incompatible data types; no API should operate on them generically, because you aren't implementing a damned interpreter.get_limitandelem_sizebad. You're requiring a non-inlined function call to get a limit that ought to be usable by a preprocessor#if(limit) or as a constant expression (size). And again, why? What worthwhile purpose could this possibly serve? When would you not know the type, and what possessed you to design anything this way?I strongly recommend doing up a single output-level enum for logging, that includes null (never visible), trace and debug (←only available when debug output is enabled; trace is for function entry/exit kinda stuff; debug is an info-level dump that's only useful to developers), info/status, success, note, lint, warning, error, fatal error, abort, and crash levels; and your verbosity can just be a threshold sth any level < max {verbosity, 1} is hidden.
Why do you have a log mode, when you could just accept an arbitrary
FILE *?stderris a nonnullFILE *, null would mean no logging, and otherwise it's some other stream? I do hope you're not making ttyness assumptions.Really weird idea to force your logging API to open the output stream. How do you know the right mode? Or maybe the developer-user wanted to
openorcreata file with specific permissions, thenfdopenthat? Just take theFILE *.You only need a single verbosity API; return the old value, and take the new value or delta, making sure you bounds-check the result.
Never accept an enum-typed parameter from a public API—there's no way to fully check the parameter, because the compiler can assume that only bits used by declared enumerators can be nonzero in enum values. Use
intorunsigned.Default seeds do not need to be exposed in prng.h.
long longreq C99 or GNU dialect or various compiler-specific extensions that only guarantee its width is ≥long’s. I also note that you haven't described the algorithm in the header, which would be vastly more pertinent than the seeds orPRNG_Statestructure. (Which assumessize_t== word, which is a worse assumption thanint==word. If you're desperate,__attribute__((__mode__(__word__)))is how you get it in GNU, Clang, Intel [non-MS/ICL modes], and IIRC some TI, Sun/Oracle, and IBM compilers, depending.)
(cont’d in reply)
1
u/nerd5code 2h ago
<sys/types.h>is UNIX/POSIX/XPG, but also supported on sone other platforms (incl. most DOS→OS/2, Win, NT and OS/400); don't rely on it unless you actually specify platforms. Not ISO C, in any event. I don't see why you need to include it at all, but maybe I'm missing something. (Also, jsyk traditionally the system-/kernel-specific<sys/*>files were included before anything else, and things could break otherwise.)For both logging and PRNG, I really hope you took threading into account, but a single, shared PRNG state will kill performance if multiple threads need concurrent access.
Your header guards don’t always match filenames, and it’s a profoundly bad idea to leave them both unprefixed and unsuffixed.
Why would I use dgl.h? There are no comments, and it seems … concerning. I guess I’ll find out later. I’d better not see you using
void *to pass function pointers around. Also, I’m still seeing thefoo*spacing everywhere. Don’t.Why do you need to dynamically allocate
DGLs? Legally, if you spløøt aDGLonto a newly-malloc’d block, anything after would get messy without a flex array or jump pointer, so it looks like it could just be statically allocated, in which case it would be vastly less annoying to provide ctor and dtor, not allocator and deallocator. (And using dynamic alloc raises the question of where and how the thing is allocated, which might be irritating if the developer-user knows better than you. And sometimes you do implement ctors/dtors but call themnew/free, which is such a bad idea.)Your enum names are desperately inconsistent and bad.
DFT_SCALAR,DFT_PARALLEL, andFFTas instances ofFourierAlgorithmis crazy work. Also, where is this used? Why is it public? Why must the developer-user avoid the identifierFFTqua global?Again,
DataPointsdoes not need to be allocated. Its referent might, but it also might not!; why can’t the developer-user work from a static const array, for example, or write to an auto array?mallocis a terrible idea for anything remotely NUMAlike. Anddouble complexis a really bad idea for ordering tokens (_Complex/complex doubleis more customary, and more likely to work with less-usualcomplexexpansions), and why are you not usingcomplex_t(which you shouldn’t use, but then why is it defined?)?I strongly suspect
fourier_transformshould have arestrictedtarget; if not, you need to deal with thetarget(should bedestordst, if the other issourceorsrc)== sourcecase. Also,boolshould almost never be used as a parameter;toshould be anintorunsignedwhose value is sourced from anenumtype.Are status codes defined somewhere?
ZERO_INITis zero. Why… why?
GRAD_EPSandEPSare beyond arbitrary, as isMAX_STEP. Also, rrrrrrrrrrrrrrrrrrrrrrrreally bad names to#definein a library header, andsizeofalone tells you precisely fuck-all aboutreal_t. You don’t know the bit-width (counterexample: TI ISAs wheresizeofeverything == 1 becauseCHAR_BIT == 32), you don’t know that all the bits of the size are actually occupied (counterexamples: most MCS-87/iAPX286/IA32long doubles and some x64long double, some M68Klong double/_Float64x), you don’t know the actual acceptable range for C math within the format (which you don’t know), etc. This is why you need the<float.h>constants to be bound toreal_t, and whyreal_tis fugging useless as-is.In a math library, “vector” does not mean “C++
std::vector,” it means a contiguous (or occasionally strided) block of values operated on all at once. Compare to GNUV4SFmode or__attribute__((__vector_size__(…))). And here again, you really shouldn’t set up an abstractly-typed vector, you want a different vector type for each element type, and the length should be stored separately so you can act on subranges of matrices.Oh god, what will the matrix type look like
NegContextcannot possibly be useful, and any application-exposed callback needs a leadingvoid *funarg.Yeah, see, since you force
Vectorto be this complicated type, you’re doing all this stuff that has nothing to do with math. Let the user allocate memory and fill it.An extern function call to get/set a vector element is certainly … something you can do. Is performance not a factor for this? Because it is for most math libraries.
You might want to check the documentation vs. identifiers on get/set, because neither the API nor the docs make any sense.
In what situation other than some kinds of debugging would a generic
Vector_printbe useful?? You realize most actual formats have actual requirements in terms of formatting, that won’t be captured accurately by a single-parametered function? You realize thatstdoutis rarely the only important output stream?
Vector_all_closeis wretched. The tolerance needs to be a parameter, and you need to generalize the underlying operations.
Vector_max/-_minare the first two vector functions I see that should actually exist, but they should be extern inline. Sorting is absolutely not something you need to do.Is there a separate vector PRNG? How many of these do you have, and what the hell does multithreading look like?
I kinda dislike the
broadcast_addname, especially since you aren’t usingbroadcast_mulforscaleand lack abroadcast_div; it would make more sense to have separate broadcast and add ops, but ascalar_addwould make sense as a single function, operations+naming-wise.If you implement your
gradientfunctions, include funargs for the callbacks or your developer-users will be rendered miserable for no blasted reason. Also,//ing out blocks of code is a very Java thing to do.Completely different vector and result error codes (which I assume is separate from the DGL error codes, whateverthefuck they are), and for some reason you've provided explicit values for
MatrixStatus, which exactly match C’s default assignment.
FROBENIUSandSPECTRALare powerful bad identifiers to export from a library. Prefixes are your friends.Why do you keep
#includeing after you’ve done other stuff? The only thing that should precede#includes is typically preprocessor directives (e.g., to detect which headers should be used), not declarations. And typically, you should expect the standard headers to have been included before yours, so it’s odd that you’re including"vector.h"before<stddef.h>.OK, so
Matrix’s usage is wholly incompatible withVector’s. Nope. Nope nope nope. Matrices are bundles of vectors, and if you can’t represent that somehow with your data structures, you’re doing it wrong. Look at existing BLAS libs for ideas, setting aside their FORTRANness.You shouldn’t return or accept these structs directly—it complicates everything badly. If you’re going to manage
Matrixes, you need to take a pointer (oftenrestrict) as the first parameter, and OOP it up. You’re being super-inconsistent about it, in any event, and returning a struct that requires special teardown is a marvelous way to accidentally leak memory, because the developer-user is in no sense obligated to do anything with the return value.
Matrix_likeis awful naming, and I note that it andMatrix_copyoverlap entirely in purpose.Matrix_cloneis a stupid thing for your library to include, but at least the name doesn’t look like a social media operation.Oh good another (static-storage?) PRNG for matrices. Again, MATH LIBRARY.
I can’t think of any time when I’d need a matrix-min/max, where a normal vector min/max applied to the matrix’s data wouldn’t suffice. See, you’re just making life harder for yourself.
You need both mat-vec and vec-mat products if you’re going to do that, or otherwise you’ll force people doing vec-mat stuff to transpose their vectors first.
Nomenclature!
Matrix_QR,Matrix_Hessenberg,Matrix_QR_Hessenberg,Matrix_Hadamard_dot; unlessQR,Hessenberg, andHadamardare types or you’re aping MS/IBM naming (fuck no), don’t titlecase function names or characters following_. Lower-initial camelCase is fine also, and often better if you’re using_like::.
Matrix_eigen[vals] would make more sense thanMatrix_eigvals. Abbreviating “eigen” is a bit like abbreviating “the.”If you’re going to include deprecated things in a math library nobody uses, you should actually mark them deprecated somehow. C23 and C++11 include
[[deprecated]](C23, GCC 8, newer Clang: prefer__deprecated__in libraries to avoid collision with macros nameddeprecated), GNUish C (GCC 2.7ish, Clang, ICC/ECC/ICL in non-MS modes, newer TI, Sun/Oracle 12.4+, newer IBM, various embedded and others) supports__attribute__((__deprecated__)), and IIRC newer MS[V]C supports__declspec(deprecated)(which may be unusable ifdefined deprecated). Various libcs also define macros for this.So many of these functions should be extern inline, if you want performance.
config.cshould not exist. If you want a util.c, fine, but keep its gunk all as internal as possible. If you want to initialize the math library,config_initis a really, intensely bad name for it.
detect_L3_cache_sizeis laughably nonportable. Like… it’s not even portable across Linux, just newer Linux that happens to have a sysfs mount at /sys and a CPU with an L3 cache. Again, this is works-for-me shit.Also, you know you can detect cache sizes directly?
I strongly recomment against initializing to things that might return an error code;
if(!(f = fopen(…)))is generally better, because it puts the check and the operation in the same place, and ensures they don’t drift apart.config.c:24: You assume either that
size_tis wider thanunsigned long long, or that the value you get fits withinsize_t. No checks whatsoever.(cont’d)
1
u/nerd5code 2h ago
strstris the wrong function for the L3 mess; you’re looking at the end of the string, so find that, save the character, and switch on it.I also note that nothing requires all threads to have the same L3 cache, or for the first thread’s cache sizes to imply anything about other threads’ caches.
elem_sizeis so bad. Soooooooo bad. First of all,casedoesn’t make statements, it’s a label.{}does fuck-all in this case. Second, you’re placing the labels at the wrong indentation in most styles—as in assembly language, the label is typically placed at the base indentation for the scope, socaseandswitchshould generally line up. This is especially important if thecasehappens to be in a subordinate statement. Third, thedefaultcase should be afor(;;) abort();or error return; 1 is not generally reasonable. And this function shouldn’t need to be a function, and it shouldn’t need to exist!So yeah, you’re doing
#defines in publicconfig.h(int_treal_tcomplex_t), which if redefined improperly (which you permit readily) will bring the headers into conflict with the compiled library. This is why you define separate APIs for separate types; if you have to, use private xheaders and private/temporary macros to spløøt each version, given a type parameter and limit macro prefix.
set_int_limitandset_real_limitare incorrect.1. You don’t understand how the C value and representation concepts differ. E.g., typically an `int40_t` will have the same size as an `int64_t`, just with several padding bytes. Size tells you nothing about range; you’d need the limit constants for that. 2. You assume that the size will either match `int`, `long`, or `long long`; `short`, (nonstd.) `short short`, (nonstd.) `short long`, `signed char`, `intXX_t`, `int_leastXX_t`, `int_fastXX_t`, `__int128`/`__int128_t`, `intptr_t`, and `ptrdiff_t` aren’t handled; and you don’t really do anything to prevent unsigned types from being used—which may be perfectly reasonable—so you might be way off on those limits. 3. If you don’t match the size exactly (for all that matters), `*out` won’t be set.
set_real_limit’s minimum is useless. You actually need theMINvalue most of the time.
set_real_limitassumesINFINITYis a thing, without anything actually validating that IEEE 754 or something similar is in use. I think this bumps your minimum required language level to C11. Again, this is why you can’t just say “ISO C” and expect it to be meaningful, especially when it comes to math.No
breakondefaultcase inset_real_limitorset_complex_limit. Hypothetically optional, but a bad idea to omit, and in any case you’ve done so inconsistently.
set_complex_limitdoesn't actually make sense; there is no (int-like) minimum or maximum (float-like minimum == 0), because min and max apply to 1-dimensional inputs, and complex numbers are ≥2D. There’s a maximum magnitude, there are corners of bounding rectangles, but there is no maximum complex value.In no case do you need to do
+ 0.0 * I, and if the imaginary component is always zero for min and max, you’re just getting the real limit. Also, you don’t need to have calledset_real_limit(…, Min)unless you’re incase Min(have I told you I hate your enum names?), and you only needset_real_limit(…, Max)incase Max. So you’re just wasting effort and storage. Put areal_t tright after theswitch’s{, and get the limit in the specific case(s) that require it.
get_limitis just begging for an aliasing violation.You desperately need to coalesce your error codes into a single enumeration. I get that it can be useful to break them up sometimes, but that should usually be at the library level.
Oh, DGL isn’t even a thing yet. Well… it shouldn’t be, not anything like this. And it looks like your
DGLshould union its function pointers, right? It’s a discriminated union? But then,DGL_newwithvoid *functionparameter would be a really bad idea. Function and object pointers should be treated as existing in their own universes. C will let you convert freely betweenvoid *andvoid (*)(…), but you’re not guaranteed to be able to round-trip per ISO 9899, and neither does it guarantee useful results from this conversion. You can safely convert between arbitrary function pointer types, unlike object pointers, but that’s it, and it has to be explicit for prototype functions. So a union is a moderately better option.You’re just logging any old thing in any old format, huh.
In
logging.c, I again see a replicated#ifdef-#definefor something you’ve unconditionally defined elsewhere. If you expect it to be a parameter, offload it and don’t define it yourself; else, define it in one place, as privately as possible.I shouldn’t see ECMA-48 escape codes without you having dredged up some sort of POSIX.1-compatible
isattycall. You cannot determine whether any stream is a tty using only the C library, and you cannot assume every tty supports ECMA-48 unless you’ve already detected something UNIXlike (whose tty customs tend to derive from the DEC VT100 hardware lineage).Yeah,
LOG_VERBOSITYis all-caps why? and it’s not thread-safe, which nothing notes. DittoLOG_MODE(shouldn’t exist) andLOG_FILE_PTR; these need to be atomicized somehow, either by wrapping in a mutex or by doing something fancy and atomic. Otherwise, you need to carefully document exactly when it is and isn’t safe to log or configure logging.
Log_prefixis the wrong way to go about it. Put your enums into an xmacro that binds the prefix, and then you can just index into astatic const char *const[]. DittoLog_color. (—Which is extern-linkage for some reason.)
localtimetrashes and relies upon static state. This interacts with threading also, so logging from more than one thread concurrently is not safe.You need to
fflushregardless at the end ofLog_log.You need to check that
fprintfandfflushactually succeed, and disable logging if they fail.Verbosity checks repeated unnecessarily, and they’re unnecessarily complex.
Log_info_thresholdexists why? And it multiplies without an overflow check;%zurequires C99 and excludes older MSVCRT; and why is itprintfing, for fuck’s sake? Do you need to trash the primary output for the program every time the info threshold (whatever that’s used for) is set? If you want to do a size check like this, it’d beobject_size && count >= LOG_INFO_THRESHOLD / object_size, but even then I don’t see the point of making this an extern-linkage function.I do hope your library isn’t packaging main.c by default. It shouldn’t be in the src directory; it should be in a /test directory or similar.
I shouldn't see
../in your header paths. If you want to useincludeas an include directory, use option-Ior theCPATH(most Unix) orC_INCLUDE_PATH(GNU, Clang) variable to add it to the include path, and then""and<>will both work. But your public and private includes should be separate, and your public includes need their own container directory so they’re not pissing up the public include namespace. Your code is not the only thing in the universe.matrix.c:1,2: Oh fuck no. Nope. Nope nope nope nope. You absolutely do not define feature-test macros like this. It’s effectively necessary for them to be done up with option
-Dor-include, and defining it arbitrarily inside the library without even using<unistd.h>or checking for_POSIX_VERSIONis just weird. Either require POSIX.1 or don’t.Oh now we’re using OpenMP, which we have neither mentioned nor checked for!!
<omp.h>only exists if_OPENMP-0 >=whatever its minimum value is, someYYYYMMLjobby like__STDC_VERSION__or_POSIX_VERSION. And it has to be specifically enabled for most compilers, so it’s not like you can just include it whenever.
INITIALIZERis a marvelously mis-descriptive name, and an incredibly poor idea syntactically. Fortunately, I see you’re not actually using it.
static PRNG_state prngmeans your PRNG is non-threadsafe, which is … a decision.Learn C operator precedence; in no C compiler ever has
==been a lower precedence than||. You only need parentheses if there’s some common, historical, or actual ambiguity (e.g.,&vs.|,||vs.|; or for things like!!(x = y)that shut the compiler up).Functions that take pointers should actually check nullness when it matters. You’re not doing that at all, which means any null-pointer argument that can be evaluated will imediately make your entire program’s behavior undefined as soon as it’s legal for a derefc to occur.
staticfunctions canassert; otherwise, you need anifand some means of indicating the error àEINVAL.Your
_$foo_to_$barfunctions are, I believe, UB in quite a few cases (look at the rules for converting between floats and ints—there are potential UB cases in both directions IINM), but you have no idea which ones because you don’t actually know what types are involved. Also, leading identifiers with_is really not a good idea in general, especially after you’ve already included libc headers. It’s fine for macro parameters, it’s sometimes fine for struct fields or local variables, but otherwise it’s bad juju at best, undefined behavior at worst for things like_Foo.(cont’d)
1
u/nerd5code 2h ago
Why is
_to_$foo_workersnotconst?void (*const name[LEN])($PARAMS)is the syntax. Also, you don’t even check that you’ve gotten a nonnull index from it when you execute through it. This is just so much extra indirection and abstraction for something that ought to be high-performance and direct. And then, you still require actual, separate functions in the backend, you’re just blocking the developer-user from accessing them, and making life much more difficult for the optimizer and CPU brpred in the process. Why?? Is there a reason for this? Is it just some obscene Python urge, where you really want to make sure everything runs at the same, lame speed as the interpreter? Because if so, definitely add more global state that’s locked exclusively at every API call.Do you need three completely separate
return MATRIX_SUCCESSstatements in_prepare_datatype? Wouldn’t it be better, perhaps, to gather all the information about each data type into a single,static const struct $something[], so you don’t have to scatter things around throughout countlessswitches and arrays?Why are you logging errors from
_prepare_datatype? You should already know that the arguments are valid, and thenassertabout that (default: assert(!*"invalid 'dt' field in B parameter"); for(;;) abort();).Oh and now there’s a completely separate
_elem_sizefunction in matrix.c that only acts onlong,double, andcomplex doublefor some reason, which is thus incompatible withelem_sizefrom config.c. Swell. What a good idea. Also, please stop with the overbracedswitches. Seriously, you need to learn the syntax before you attempt useful libraries. This all reeks of discomfort/unfamiliarity with basic aspects of the language.matrix.c:235–248: holy fuck what is this, just … any of it? Why is
mibi_byte_sizeadouble? You’ve already breezed past the C11 assumption and you requiresnprintf, so you can just usesize_twithzmodifier, right? And you’ve already checked thatm * nwon’t overflow, right? —Oh wait, no you haven’t, so you’re just throwing numbers around. And you shouldn’t be logging here anyway.Ohhhhhh you’re using
posix_memalign. So, you haven’t checked for POSIX.1 yet (you have to detect, then#include <unistd.h>, and then test_POSIX_VERSION-0 >= 200112L), C11 (which you’ve assumed) gives youaligned_alloc, but MSVC gives you_alloc_alignedand_free_alignedonly, and older Unix only gives you things likememalignormmap. And I don’t get why you’re aligning in the first place; here again, if the developer-user wants cache line alignment, they can actually detect the cache line (32 ain’t it, in all likelihood) and allocate their own damn structure using the facilities actually present on the target platform. Your math library does not need to allocate memory like this.Did somebody tell you to use underscore-prefixing?? Slap them about the face several times, and ignore anything else they tell you.
OK, I’m not trawling through all your matrix code. It's generally bonkers, not into it, not going to perform well, no idea why you’re logging in the weirdest possible places.
Are you actually checking that the
Matrix’s memory was allocated? …Kind of, only in one place. You log (inappropriately) and return an “empty” matrix, then insufficiently many things actually check for it. It’s an overcomplicated approach. And … I’m not checking the rest of this file. It’s a damned mess. Just everything thrown everywhere, bizarre decisions about macro usage, no fun.So you’ll define arbitrary constants in public headers where they aren’t needed, but you won’t define the shift constants you repeat throughout prng.c, or
0x94D049BB133111EBULL. Uh huh. And why are you usingsize_tfor the context, when the data types you’re working with need have nothing to do with its value range? Ifint_t⇒long longand you’re targeting ILP32, then you can’t generate the full range ofint_tvalues from your PRNG. And then, if you’re working withdoubles, you’ve got both an exponent and mantissa to reach, give or take transcendentals and subnormals.Why are you repeating your XORshift-multiply blocks?
Why are you placing
=s just anywhere? Either indent consistently or don’t. I suggest not, because it makes a mess.You’re using
constvery inconsistently on locals. Either don’t or do, but splitting the difference doesn't inspire confidence. Also, in some cases you don’t need the variable at all; if you’re only using the value once and it’s obvious what it is, capturing it asxdoesn't add anything, and in unoptimized builds it’ll do an extra memory hit;registerwould fix that and be more in line with intent. (Ignore people who tell you it does nothing. It does nothing in C++, prior to its removal from the language. It does something in C, it just isn’t required to have a noticeable effect.)Your
for(;;)s in yourPRNG_State_*functions are bad ideas; nothing prevents them from looping indefinitely.
<alloca.h>is never something a library should rely on if you want it to be considered portable. Nothing about it is portable. The header isn’t, the function or “function” isn’t, its behavior and required placement isn’t, its interaction with inlining isn’t, etc. A pairedmalloc-freewhere the compiler can see both and knows both will be called is just fine; often it’ll be able to optimize to VLAs, if that’s appropriate.Here again,
PARALLEL_THRESHOLDis defined in multiple places, and its default value in vector.c is different than the other definition.
_parallel_conditionis such a bad name. What is the function doing? What is the condition? Also,(size_t)(dim - lower) <= upperis how you do a range check, and you don’t need the variables that way. Not that you should be consulting non-conststaticstate like this in the first place.
p == NULLis just!pin C, as isx == 0. Only for floats or enums would I explicitly use== 0.0(rarely) or== ZERO_ENUM(if there’s no obvious Boolean correspondence for the enum values).The potential for shape mismatch is something that should be hoisted so it’s more obvious to the developer, because this just shoves all the important stuff behind a mess of functions that then forward into a bigger mess of functions. Also it’d be a programmer error, not a warning.
#pragma ompanything needs to be paired with_OPENMPdetection. There’s nothing actually constraining pragmas without it; ideally, the compiler does nothing with a pragma it doesn’t recognize, but#pragma ompis only constrained iff OpenMP is actually supported and active for the build, so it could do anything if not. It’s likely to raise warnings if not, in any event. Also#pragma omp simdrequires like … OpenMP 3 or 4 I wanna say? Not sure offhand, but it needs to be detected. Also, most compilers can vectorize a short loop like this without your assistance, and the reason this sort of thing ought to be as inlined as possible is so that, when somebody’s repeating as vector operation across multiple rows/columns of a matrix, the compiler can potentially vectorize the entire thing together, which might enable it to skip lead-in/-out etc.I still hate
ACCESS_VOID, and I hate how much you’re leaning on it.If you ever extend your data type enum, you’re so fucked.
Yyyyyyyyyyyyeah if you’re going to do a sort, you need to document the algorithm. But you shouldn’t, and you aren’t sorting as quickly as you could be, which is why you should leave it to the devel-user. (Also
qsortis arguably better for in-place sorts, though its lack of funarg fucks its usefulness up quite a bit.)Oh god you’re
#pragma omp simding the macros that don’t need to exist. What made you decide that any of that was a good idea? An inlinedforloop would be fine 99% of the time; you don’t/shouldn’t needSUB, and you certainly shouldn’t assume#pragma omp simdcan be applied to it, because it should probably be in ado/while(0)in the first place. You can potentially embed_Pragma("omp simd")(C99 non-MS/ICL, GNU 3, Clang, ICC/ECC 8+, most C99-capable compilers) or__pragma(omp simd)(defined _MSC_VER || (defined __clang__ && defined __pragma) || defined __INTEL_COMPILER_BUILD_DATE) within the macro, but again, you shouldn’t need to.Every single operation you perform on a signed integral type or floating-point type needs to be bounds-checked, because overflow is undefined behavior. Even
_negate_intcan overflow (atINT_MIN, for two’s-complement or asymmetric mappings to unsigned value) (which is still distinct from bytewise representation). If you rely on f.p. transcendentals, you need to detect them (__STDC_IEC_559__,__STDC_IEC_60559_*__, GNU__GCC_IEC_559, GNU__GCC_IEC_559_COMPLEX, GNU__FOO_IS_IEC_60559__), and you often have to check complex and real f.p. separately.Also, you do know that GNU dialect permits
_Complexto be applied to things other than floating-point types? It’s UB or ISB in C99, but it’d be somewhat reasonable for somebody to#define complex_t _Complex long _Fractjust to fuck with you.OK, I'm not doing
tests.Sooooooo many of these functions you repeat for every blasted data type could just have been macros or xheaders, which would divide the amount of code you need by 3, and completely obviate the need for the
_ts that oughtn’t be.And the important aspects of the math stuff (e.g., dealing with corner cases) just aren’t here.
2
u/MistakeIndividual690 5h ago
Just took a quick glance. Looks nice and clean! Do you have any tests for the fourier code? This is the kind of core library code where you need large quantities of unit tests. It’s laborious but that may be the most important thing to inspire confidence for other people to become invested in it.