Register a SA Forums Account here!
JOINING THE SA FORUMS WILL REMOVE THIS BIG AD, THE ANNOYING UNDERLINED ADS, AND STUPID INTERSTITIAL ADS!!!

You can: log in, read the tech support FAQ, or request your lost password. This dumb message (and those ads) will appear on every screen until you register! Get rid of this crap by registering your own SA Forums Account and joining roughly 150,000 Goons, for the one-time price of $9.95! We charge money because it costs us money per month for bills, and since we don't believe in showing ads to our users, we try to make the money back through forum registrations.
 
  • Post
  • Reply
cheetah7071
Oct 20, 2010

honk honk
College Slice
The actual loop is something like

code:
while(true) {
	mut.lock();
	if (sharedInt > sharedVector.size()) {
		mut.unlock();
		break;
	}
	auto myTask = sharedVector[sharedInt];
	sharedInt++;
	mut.unlock();
	//do stuff asynchronously on myTask, occasionally locking mut to update shared variables
}
I just cut down on all that for brevity--it was relevant to the post that it happened in the middle of a loop, I thought, but not exactly what the loop was. A lot more than just a mutex is being shared between these threads.

cheetah7071 fucked around with this message at 01:15 on May 5, 2022

Adbot
ADBOT LOVES YOU

cheetah7071
Oct 20, 2010

honk honk
College Slice
honestly there's probably a perfect object in the std library for this kind of task splitting code that skips all the boilerplate but I didn't want to go into a bunch of research when I knew how to do it manually and it's a very simple snippet

Absurd Alhazred
Mar 27, 2010

by Athanatos

cheetah7071 posted:

The actual loop is something like

code:
while(true) {
	mut.lock();
	if (sharedInt > sharedVector.size()) {
		mut.unlock();
		break;
	}
	auto myTask = sharedVector[sharedInt];
	sharedInt++;
	mut.unlock();
}
I just cut down on all that for brevity--it was relevant to the post that it happened in the middle of a loop, I thought, but not exactly what the loop was. A lot more than just a mutex is being shared between these threads.

I'm not sure why you're not just lock::guarding that whole while block. That break is still going to leave the block, destructing the guard anyway.

cheetah7071
Oct 20, 2010

honk honk
College Slice

Absurd Alhazred posted:

I'm not sure why you're not just lock::guarding that whole while block. That break is still going to leave the block, destructing the guard anyway.

that's basically what I switched to after I got the first replies. I just declared myTask outside of an anonymous scope and stuck the lock logic inside of it. The whole issue was that I didn't stick it in a scope to begin with because I was declaring a variable inside of it, which is very simple to get around once I understood what I needed to do.

ExcessBLarg!
Sep 1, 2001

cheetah7071 posted:

honestly there's probably a perfect object in the std library for this kind of task splitting code that skips all the boilerplate but I didn't want to go into a bunch of research when I knew how to do it manually and it's a very simple snippet
So you're implementing a work queue to be run in a thread pool? I see two potential issues with this:

1. If the number of tasks is fixed before you split the work, it's may be better to distribute the tasks into multiple queues and hand each thread its own queue so there's no shared state. If you share a single queue across all threads then you'll potentially lose a fair bit of performance to lock contention, unless your processing time is quite long per task. Conversely if you use separate queues then some threads may finish early if the processing time isn't consistent. Which approach is better depends on if your processing time is short but consistent, or if it's long and potentially inconsistent. There's also work-stealing techniques that work with either kinds of tasks but they take more effort to implement.

2. If your number of tasks is not fixed and you may potentially be adding to the work queue while also processing, then you need to tolerate the condition that the queue may potentially be empty, but eventually refill. For that you would need a condition variable and termination signal as part of the while loop.

ExcessBLarg! fucked around with this message at 01:43 on May 5, 2022

ExcessBLarg!
Sep 1, 2001

Absurd Alhazred posted:

I'm not sure why you're not just lock::guarding that whole while block. That break is still going to leave the Ablock, destructing the guard anyway.
Because processing of myTask is slow and holding the lock would prevent other threads from obtaining tasks in a parallel fashion.

cheetah7071
Oct 20, 2010

honk honk
College Slice

ExcessBLarg! posted:

So you're implementing a work queue to be run in a thread pool? I see two potential issues with this:

1. If the number of tasks is fixed before you split the work, it's may be better to distribute the tasks into multiple queues and hand each thread its own queue so there's no shared state. If you share a single queue across all threads then you'll potentially lose a fair bit of performance to lock contention, unless your processing time is quite long per task. Conversely if you use separate queues then some threads may finish early if the processing time isn't consistent. Which approach is better depends on if your processing time is short but consistent, or if it's long and potentially inconsistent. There's also work-stealing techniques that work with either kinds of tasks but they take more effort to implement.

2. If your number of tasks is not fixed and you may potentially be adding to the work queue while also processing, then you need to tolerate the condition that the queue may potentially be empty, but eventually refill. For that you would need a condition variable and termination signal as part of the while loop.

I'm happy to be educated on this subject! You're correct that this is a situation where the run time is inconsistent. It's not that bad for this specific project (run time per task is 5-30 seconds), but I've had it be really really bad in previous projects where my tasks would vary from 5-30 minutes, and I just got in the habit of writing my work queue this way. Still, at the upper end, the number of tasks is in the tens of thousands so even variances of a few dozen seconds can potentially add up. For this project, the shared variables are basically only ever referenced at the very beginning of the loop (to get the next task) and at the very end (to log how much work was done). Probably measured in nanoseconds.

Absurd Alhazred
Mar 27, 2010

by Athanatos

ExcessBLarg! posted:

Because processing of myTask is slow and holding the lock would prevent other threads from obtaining tasks in a parallel fashion.

I must have miscommunicated. I meant:

C++ code:
while(true) {
	std::lockguard lk(mut);
	if (sharedInt > sharedVector.size()) {
		break;
	}
	auto myTask = sharedVector[sharedInt];
	sharedInt++;
}
It shouldn't make a difference, it's still going to unlock at the end of the block.

If you mean there's extra steps for myTask after what would have been the mut.unlock() after sharedInt++ then I guess the solution could be something like:
C++ code:
while(true) {
	std::optional<TaskType> myTaskOptional;
	{
		std::lockguard lk(mut);
		if (sharedInt > sharedVector.size()) {
			break;
		}
		myTaskOptional = sharedVector[sharedInt];
		sharedInt++;
	}
	TaskType& myTask = *myTaskOptional;
	// more myTask work
}

cheetah7071
Oct 20, 2010

honk honk
College Slice

Absurd Alhazred posted:

I must have miscommunicated. I meant:

C++ code:
while(true) {
	std::lockguard lk(mut);
	if (sharedInt > sharedVector.size()) {
		break;
	}
	auto myTask = sharedVector[sharedInt];
	sharedInt++;
}
It shouldn't make a difference, it's still going to unlock at the end of the block.

If you mean there's extra steps for myTask after what would have been the mut.unlock() after sharedInt++ then I guess the solution could be something like:
C++ code:
while(true) {
	std::optional<TaskType> myTaskOptional;
	{
		std::lockguard lk(mut);
		if (sharedInt > sharedVector.size()) {
			break;
		}
		myTaskOptional = sharedVector[sharedInt];
		sharedInt++;
	}
	TaskType& myTask = *myTaskOptional;
	// more myTask work
}

That second implementation is more or less exactly what I did. I forgot to include the comment saying to process myTask originally, and edited in. In the original post it was very unclear what I was even doing with it.

Absurd Alhazred
Mar 27, 2010

by Athanatos

cheetah7071 posted:

That second implementation is more or less exactly what I did. I forgot to include the comment saying to process myTask originally, and edited in. In the original post it was very unclear what I was even doing with it.

Oh, okay, now I understand.

ExcessBLarg!
Sep 1, 2001

cheetah7071 posted:

You're correct that this is a situation where the run time is inconsistent.
I think you have the right approach then.

There might be some work queue stuff in Boost but honestly there's a lot of nuance to how to implement them (well) depending on the nature of the tasks.

I've recently came across a project where I needed a work queue to tolerate short and inconsistent duration tasks so I had to brush up on my literature.

roomforthetuna
Mar 22, 2005

I don't need to know anything about virii! My CUSTOM PROGRAM keeps me protected! It's not like they'll try to come in through the Internet or something!

ExcessBLarg! posted:

I think you have the right approach then.

There might be some work queue stuff in Boost but honestly there's a lot of nuance to how to implement them (well) depending on the nature of the tasks.

I've recently came across a project where I needed a work queue to tolerate short and inconsistent duration tasks so I had to brush up on my literature.
What sort of work has tasks so short that they cause significant and performance-hindering contention on a simple deque-based queue lock, but is also worth offloading to a thread pool in the first place?

Jabor
Jul 16, 2010

#1 Loser at SpaceChem
Some sort of per-pixel transformation that you don't want to shuffle all the data for over to a GPU, perhaps.

But in that case you could batch up the work with coarser granularity than a single pixel, so idk.

ExcessBLarg!
Sep 1, 2001

roomforthetuna posted:

What sort of work has tasks so short that they cause significant and performance-hindering contention on a simple deque-based queue lock, but is also worth offloading to a thread pool in the first place?
A recursive constraint satisfaction solver where instead of recursing at each step of the search you insert additonal tasks in a work queue and use a number_of_cores-sized thread pool to perform the search.

I played around with things like limiting the search depth of tasks inserted into the queue (searches with greater depth would just recurse in the same work unit), but that sort of thing results in a lot of parameter tweaking that doesn't necessarily carry across the problem sets.

In literature I found most discussion of work-stealing-queues to be relevant to HPC.

Rocko Bonaparte
Mar 12, 2002

Every day is Friday!
Has anybody ever considered trying to add a fake device in QEMU for integration/regression testing of drivers for the real device? I was curious how much of a thing that is.

Beef
Jul 26, 2004
Try giving Intel TBB a go if you need tasking through a thread pool in C/C++. As with all things concurrency, it's hard to get right yourself.

more falafel please
Feb 26, 2005

forums poster

roomforthetuna posted:

What sort of work has tasks so short that they cause significant and performance-hindering contention on a simple deque-based queue lock, but is also worth offloading to a thread pool in the first place?

Have you heard about video games?

Beef
Jul 26, 2004
Graph analysis and DBs

csammis
Aug 26, 2003

Mental Institution

Rocko Bonaparte posted:

Has anybody ever considered trying to add a fake device in QEMU for integration/regression testing of drivers for the real device? I was curious how much of a thing that is.

We’ve started exploring Renode for this at my workplace and it’s looking very promising, but we work almost exclusively on ARM cores so YMMV.

Rocko Bonaparte
Mar 12, 2002

Every day is Friday!

csammis posted:

We’ve started exploring Renode for this at my workplace and it’s looking very promising, but we work almost exclusively on ARM cores so YMMV.

Yeah that looks like a nice thing, but I have to work exclusively on regular Intel desktop and server stuff. If I read it right, Renode can do Intel Quark but that's it.

mmkay
Oct 21, 2010

Rocko Bonaparte posted:

Has anybody ever considered trying to add a fake device in QEMU for integration/regression testing of drivers for the real device? I was curious how much of a thing that is.

Yes, but it was a type of NVMe device, for which adding the extra tidbits/logic was pretty trivial.

Qwertycoatl
Dec 31, 2008

Rocko Bonaparte posted:

Has anybody ever considered trying to add a fake device in QEMU for integration/regression testing of drivers for the real device? I was curious how much of a thing that is.

It's definitely a thing. I'm a bit fuzzy on the details but where I work we have a separate program which connects to qemu over a socket and pretends to be a PCIe device

Beef
Jul 26, 2004
Same, our group is doing something similar. QEMU connects to the device's simulator over a TCP socket and is seen by the guest OS as a PCIe device. It really helped to hit the ground running when the real HW arrived, with a driver etc. ready to go.

cheetah7071
Oct 20, 2010

honk honk
College Slice
I have a sort of unusual nested set of dependencies which are giving me headaches.

Library A includes library B, but doesn't surface the API. Since I need both APIs, I have dependencies on both

Library A has some symbols defined in its .lib file which library B tries to directly #include. This, understandably, results in a linker error for multiple definition. If I comment out that #include in the source header for library B, everything works fine--my own project can successfully link to library A's .lib file to get those definitions and library B is satisfied with that

So my question is, is there any kind of fuckery I can do to fix this without modifying the header of library B? Like a preprocessor directive I put at the top of my own code that means "don't include file X no matter what" or something.

It is possible to compile library A with a switch to not define those symbols and resolve the whole issue, but when I tried I got a gigantic pile of cmake errors and I'm hoping there's an easier solution

e: it occurs to me I can just define the include guard macro from that file to prevent it from doing anything. Thanks for being my rubber duck, thread

cheetah7071 fucked around with this message at 06:59 on May 10, 2022

ExcessBLarg!
Sep 1, 2001
Compiling and linking are separate stages. You shouldn't get a linker error as a result of including a header file twice across separate translation units--this is regularly done. The only way I can think of that, that would happen is if the header defines a symbol instead of just declaring it which (weird inline edge cases aside) headers shouldn't be doing.

Are you getting a compiler error from the two headers declaring the same struct/whatever twice with slightly different definitions? If that's the case you may have to do something like you're doing. Sometimes even just reordering your include statements will fix it.

cheetah7071
Oct 20, 2010

honk honk
College Slice
Library A included this file and was compiled into a .lib file with the symbols of that header file surfaced, in the .lib. So that file is never directly included if you just link against the library--I assume one of the headers forward declares the symbols in another file, though I haven't checked. Library B directly includes the file in its API headers.

At least that's the only explanation I can come up with for why it's a linker error and not a compiler error lol

e: library A is kind of a weird case in that it's basically a wrapper for like 50 other libraries, presenting a unified API for them. I'm directly including one of those internal libraries because I need one of the functions in it, but that essentially means I'm including the library twice--once in a completely normal way and once in a weird indirect way. I'm not too surprised there's bizarre errors tbh.

cheetah7071 fucked around with this message at 17:17 on May 10, 2022

ultrafilter
Aug 23, 2007

It's okay if you have any questions.


What's the actual error message from the linker?

cheetah7071
Oct 20, 2010

honk honk
College Slice

ultrafilter posted:

What's the actual error message from the linker?

oh it is a compiler error and I was misremembering because it was the middle of the night, apologies

C2365, redefinition of a variable

The file in question does have include guards but I've definitely had errors like this when defining symbols in my headers in my own projects (hence the usual advice to make all functions in headers inline to avoid the issue)

ExcessBLarg!
Sep 1, 2001

cheetah7071 posted:

C2365, redefinition of a variable
OK, so what are the two definitions of the variable?

cheetah7071 posted:

The file in question does have include guards but I've definitely had errors like this when defining symbols in my headers in my own projects (hence the usual advice to make all functions in headers inline to avoid the issue)
You shouldn't place functions in headers unless they're inline (and even then there's compiler-specific issues regarding inline vs. static inline and emitting exactly one definition of the function for external linkage). This isn't advice, headers shouldn't contain function definitions full stop.

chglcu
May 17, 2007

I'm so bored with the USA.
It sounds like maybe theres an externally visible variable being defined in the header. If so, you could mark it extern in the header and move the definition into exactly one cpp file in the library the header belongs to, preventing multiple definitions.

cheetah7071
Oct 20, 2010

honk honk
College Slice

ExcessBLarg! posted:

OK, so what are the two definitions of the variable?

Error C2365 'CE_None': redefinition; previous definition was 'enumerator'

There's about 5 of these. All of them are for error codes or for the enum that they belong to

the actual code in the library header causing the issue is:

code:
typedef enum
{
    CE_None = 0,
    CE_Log = 1,
    CE_Warning = 2,
    CE_Failure = 3,
    CE_Fatal = 4
} CPLErr;
The rest of the header is just function declarations causing no issues.

quote:

You shouldn't place functions in headers unless they're inline (and even then there's compiler-specific issues regarding inline vs. static inline and emitting exactly one definition of the function for external linkage). This isn't advice, headers shouldn't contain function definitions full stop.

Yeah I know, and I only ever do it when they're templated functions--and this was something I learned years ago, I was just saying that the symptoms look similar to what I was getting before I learned that

cheetah7071
Oct 20, 2010

honk honk
College Slice

chglcu posted:

It sounds like maybe theres an externally visible variable being defined in the header. If so, you could mark it extern in the header and move the definition into exactly one cpp file in the library the header belongs to, preventing multiple definitions.

none of these files are my code--they're all external libraries I'm including as dependencies that I'd rather not touch

chglcu
May 17, 2007

I'm so bored with the USA.

cheetah7071 posted:

Error C2365 'CE_None': redefinition; previous definition was 'enumerator'

There's about 5 of these. All of them are for error codes or for the enum that they belong to

the actual code in the library header causing the issue is:

code:
typedef enum
{
    CE_None = 0,
    CE_Log = 1,
    CE_Warning = 2,
    CE_Failure = 3,
    CE_Fatal = 4
} CPLErr;

That seems to me like something an include guard should have prevented, if there’s not another CE_None somewhere else in the code.

cheetah7071
Oct 20, 2010

honk honk
College Slice

chglcu posted:

That seems to me like something an include guard should have prevented, if there’s not another CE_None somewhere else in the code.

Ah, you're right, when I right click go to definition for CE_None, I end up in a completely different file with an identical definition for the same enum

that's a bit troublesome

e: I only need a tiny little bit of library B, and it doesn't seem to be using anything besides the enum in that header, so defining the include guard macro to prevent the multiple definition is both compiling and passing tests

It's pretty inelegant but I'm not sure what else I'm supposed to do when two C libraries decide to use the same name for their objects

cheetah7071 fucked around with this message at 18:24 on May 10, 2022

ExcessBLarg!
Sep 1, 2001
Again, this isn't a duplicate symbol issue, and it's not a linker error. The problem is having two definitions of the same enum in the same translation unit.

I'm a little surprised this is an issue and that the compiler doesn't just see an identical definition and go "oh, OK". That said, based on how you describe this is happening I think what you have is a reasonable solution. Both libraries should define APIs in their respective header files, but where the APIs share a common data structure they should be referencing the same underlying header file which would have include guards to prevent being included twice. It sounds like libA might've copied some type definitions into its own header though which is why that didn't work.

Presto
Nov 22, 2002

Keep calm and Harry on.

ExcessBLarg! posted:

You shouldn't place functions in headers unless they're inline
Note: Functions defined inside the class definition are automatically inline.

StumblyWumbly
Sep 12, 2007

Batmanticore!
Does anyone use C++ for embedded?

I've done C since the dawn of time, working in embedded systems at small companies for a while now. I'm starting to look at larger companies for a change, and it looks like they're much more into C++, which makes sense when you're working in larger teams with more power.

How deep into C++ do embedded systems go? It sounds like elements of boost and, I assume, many other libraries are available. I've always heard templates are inherently unsafe in memory limited environments, but looking into it that might just apply to making things arguably worse in unstable memory environments, and writing C++ correctly (compared to C) has essentially the same execution parameters and is faster to write.

Or does it really depend on the size of the system, so a Pixel runs C++ and a fit bit runs essentially C?

Foxfire_
Nov 8, 2010

StumblyWumbly posted:

Does anyone use C++ for embedded?
I work on a thing running on a smallish microcontroller (Cortex M7, no heap, no OS) that is written mostly in C++. You have to know something about how language features will be implemented and which ones are not suitable (e.g. don't go all crazy with std::string). Many language features with no C equivalents are useful though.

Since you mention templates specifically, there's nothing wrong with using templates in embedded code. You should be aware that instantiating them with new types will generate additional code size, but template-generated code is not going to be any bigger or worse than the equivalent manually template-expanded C code, just less repetitive to write. (I don't know what you mean by "unstable memory environment")

e: more concrete but still contrived example:

Suppose you want a pair of queues in your program, one with a max size of 10 & holding elements of type struct Foo, and the other with a max size of 20 & holding elements of type struct Bar. That's a pain in the rear end to write with C code, but straightforward with C++ templates + the template generated version isn't going to be any bigger code size than the handwritten one

Foxfire_ fucked around with this message at 05:31 on May 11, 2022

qsvui
Aug 23, 2003
some crazy thing

StumblyWumbly posted:

Does anyone use C++ for embedded?

I've done C since the dawn of time, working in embedded systems at small companies for a while now. I'm starting to look at larger companies for a change, and it looks like they're much more into C++, which makes sense when you're working in larger teams with more power.

How deep into C++ do embedded systems go? It sounds like elements of boost and, I assume, many other libraries are available. I've always heard templates are inherently unsafe in memory limited environments, but looking into it that might just apply to making things arguably worse in unstable memory environments, and writing C++ correctly (compared to C) has essentially the same execution parameters and is faster to write.

Or does it really depend on the size of the system, so a Pixel runs C++ and a fit bit runs essentially C?

Embedded systems are pretty broad but I'm gonna assume you mean bare metal and not Linux based on the rest of your post.

I've worked in places that used C++ for bare metal. They weren't shy about using C++ features as there was plenty of code that used template metaprogramming, extensive inheritance hierarchies with virtual functions, you name it. Funny you mention Boost, because I have written code for microcontrollers that used Boost libraries. The only restrictions I've encountered were no exceptions/RTTI and the usual no dynamic heap allocation.

Regarding templates being memory hogs, I've heard compilers 20 or 30 years ago may have had these problems but modern compilers should be better about this. Of course, I was only working on ARM based systems which have great compiler support. A lot of proprietary embedded compilers may have poor or no support for C++ so you might be stuck with C for these tools.

There's no reason why C++ can't be used on small systems. I'd say C++ would even be a better choice because of the greater compile-time programming support it has. Not to mention just the quality of life improvements like the STL, lambdas, RAII, std::optional, etc.

Adbot
ADBOT LOVES YOU

yippee cahier
Mar 28, 2005

I am writing C++ for a Cortex-M0+ that’s not very powerful. We can’t use dynamic memory or exceptions in this environment, so the STL is out. It’s, as you note, cleaner and faster to write with the same execution if done correctly. I have objects that represent GPIO pins and have a bunch of inlined member functions. They take up 8 bytes of RAM if not inlined themselves. Templates are great, if you understand what the compiler is going to stamp out it’s just saving time and lines of code with no downsides.

I too came from a C background and think it was a huge help. You know how things should be under the hood and won’t be naive mistakes that gobble up memory. You just get to sit back and appreciate all the boilerplate C you’re not writing.

The other side of the fence is embedded linux, where in my experience the application development is pretty normal. No real constraints. The embedded skills there were more about getting hardware to be recognized and show up to have an environment to run very normal software on. YMMV, of course.

  • 1
  • 2
  • 3
  • 4
  • 5
  • Post
  • Reply