LLVM Techniques, Tips, and Best Practices Clang and Middle-End Libraries: Design powerful and reliable compilers using the latest libraries - podcast episode cover

LLVM Techniques, Tips, and Best Practices Clang and Middle-End Libraries: Design powerful and reliable compilers using the latest libraries

Oct 09, 202525 min
--:--
--:--
Download Metacast podcast app
Listen to this episode in Metacast mobile app
Don't just listen to podcasts. Learn from them with transcripts, summaries, and chapters for every episode. Skim, search, and bookmark insights. Learn more

Episode description

A comprehensive guide to the LLVM compiler infrastructure. It covers various aspects of LLVM, including its build system, testing utilities like LIT, and the domain-specific language TableGen. The text explores frontend development with Clang, explaining its architecture, preprocessing, Abstract Syntax Tree (AST) handling, and custom toolchain creation. Furthermore, it details working with LLVM's PassManager and AnalysisManager for code optimization, processing LLVM Intermediate Representation (IR), and developing sanitizers and Profile-Guided Optimization (PGO) within the LLVM framework.

You can listen and download our episodes for free on more than 10 different platforms:
https://linktr.ee/cyber_security_summary

Get the Book now from Amazon:
https://www.amazon.com/Techniques-Practices-Clang-Middle-End-Libraries/dp/1838824952?&linkCode=ll1&tag=cvthunderx-20&linkId=9e9b6879b593c7a46efbdaf46b4e4276&language=en_US&ref_=as_li_ss_tl

Discover our free courses in tech and cybersecurity, Start learning today:
https://linktr.ee/cybercode_academy

Transcript

Speaker 1

Okay, let's unpack LLVM. It's incredibly powerful, right, underpins so much stuff, apples, x code, game engines, you name it.

Speaker 2

Absolutely it's everywhere.

Speaker 1

But you know, for developers already deep in the ecosystem or maybe looking to get deeper into compiler engineering, tackle the docs, well, it can feel less like learning and more like drinking from a fire hose.

Speaker 2

That's a perfect description. It's famously scattered, right.

Speaker 1

So our source today, this book LLVM Techniques, tips and bust practices, it promises to sort of rain that beast in. So what's our big mission for you today? What are we trying to achieve here?

Speaker 2

Well, our mission is really to give you this streamlined, comprehensive overview to cut through all that documentation sprawl. We're going to unearth some key techniques, maybe some surprising insights, to help you build, tests, optimize, and importantly debug your LLVM projects much more efficiently. Fewer headaches.

Speaker 1

Fewer headaches are always good.

Speaker 2

Definitely think of this deep dive as like your shortcut, making LLVM development faster, more reliable, and well more insightful. We're aiming straight at those common pain points, those really long build times, complex testing opaque debugging, that kind of thing.

Speaker 1

Yeah, long build times. That's off for the first mountain you hit, isn't it. Especially with a huge project like LLVM, defaults can take hours, huge productivity killer.

Speaker 2

Oh absolutely, it's a major drag.

Speaker 1

So what's the secret? How do we cut down this build time? Beast?

Speaker 2

Okay, so the book immediately points to replacing slower, older tools. Makes sense, right, right? Take build systems Ninja. It just runs significantly faster than say, g and you make on big code bases. LLVM is a perfect example.

Speaker 1

Why is Ninja faster? What's the magic there?

Speaker 2

The key is it's build dot ninjascript. It's it's almost like assembly language for builds. It gets generated by higher level systems like Seamake, and because it's so low level, it allows for loads of optimizations under the hood. Plus it handles dependencies much better. You just tell Sea make desh g ninja as simple as that.

Speaker 1

So just one command line flag can make a big difference. We're basically upgrading the engine. But it's not just the build system, is it. The linker is often a problem too.

Speaker 2

Precisely, the default linker BFD. It's mature, sure, but it wasn't really built for modern speed or memory needs.

Speaker 1

How bad can it get?

Speaker 2

It can show up that this up to twenty gigabytes of memory building LLVM.

Speaker 1

Wow. Okay, that's a bottleneck.

Speaker 2

Definitely a performance hurdle. But thankfully there are much better alternatives g and you gold, Google developed that one, and LVM's own linker ld LED Yeah. Ld is often even faster, and it's got experimental parallel linking too, which is pretty cool. And again easy sea make flags ds DLVM muslinker Gold or dz dlv musle ankored. Okay, the speed up from just those two changes Ninja and a faster linker, it's substantial, really noticeable.

Speaker 1

That's huge just swapping out a couple of underlying tools. But we can also tweak c make itself right, fine tune the build arguments.

Speaker 2

Absolutely. Tweaking CE make arguments is crucial for efficiency, like choosing the right build type. A roll with deb info is off in the sweet spot. Why that one, Well, it gives you optimized code so it's fast, but it keeps the debug information so you get a great balance between space speed and you know, being able to actually debug.

Speaker 1

It makes sense.

Speaker 2

You generally want to avoid a full debug build unless you absolutely have to. It just creates so much unnecessary storage waste, huge binaries and targets.

Speaker 1

LVM supports what nearly two dozen hardware targets. Most people don't need all of those really exactly.

Speaker 2

That's another massive time sink. If you build them all. You can save a ton of time by just building the ones you actually need. Use DLLLVM target.

Speaker 1

Still build How does that look?

Speaker 2

Something like Della's DLVM targets to build X eighty six R sixty four. Just list the ones you care about.

Speaker 1

And there's a catch with shells you mentioned.

Speaker 2

Ah yeah, good point. In some shells like BSh, you have to remember the double quotes around the list, otherwise the command gets cut off part way through. Little gotcha?

Speaker 1

Good tip? What else? Shared libraries?

Speaker 2

Yes, another great strategy, especially during development. Build LVM components as shared libraries. Use LVM components as shared libraries.

Speaker 1

Use aad build shared libs.

Speaker 2

On Why is that better? Because LVM is so modular, Building shared library saves a significant amount of storage space and really speeds up the linking part of the build process compared to static libraries. Much faster iteration.

Speaker 1

Okay, and what do LVM dash tubulliging. That one comes up a lot as being slow.

Speaker 2

It does. It can really impact build times. But there's a trick. You can build an optimized version of just lavmt bulgein itself even if the rest of your build is in debug mode. Use h dlll V optimized stable gen.

Speaker 1

Eldve one nice. So optimizing the tool that helps build the.

Speaker 2

Tools exactly, it shaves off more time.

Speaker 1

So it really feels like we're swapping out a rusty old tractor for a soup up racing machine just by changing a few settings and tools. Speaking of alternatives, the source mentioned another build system gn it.

Speaker 2

Does generate Ninja or gn used a lot by Google projects. It's like Chromium.

Speaker 1

What's its advantage.

Speaker 2

It's known for really fast configuration time and reliable argument management. The book says it's especially useful if your developments make changes to build files, or if you're constantly trying out different build options. Much quicker reconfiguration, so.

Speaker 1

Good for rapid iteration on the build itself exactly.

Speaker 2

It's more of an alternative for those specific scenarios. Maybe not a full replacement for everyone, but very handy when you're tweaking build files a lot.

Speaker 1

Okay, it makes sense. So once you've got your compiler built fast, the next big hurdle is reliability testing. How do you make sure it's actually, you know, correct?

Speaker 2

Yeah, testing is critical, and LVM provides its own framework for this, LVM LIT like LLVM Integrated Tester. The book calls it an easy to use yet general framework, and importantly, while it started for LVM's own tests, it's actually a generic testing framework. You can use it outside LVM for other projects too.

Speaker 1

Very versatile and inside RIT there's this utility file check that sounds key for compiler testing. What's special about it?

Speaker 2

File check is really powerful. It does advance pattern checking on output files. It goes way beyond just diffing text, so you embed directives right in your test files. Gacheck is basic rajex matching, simple enough, but then you get directives like check next t that makes sure a pattern is found on the very next line after the previous match. Super useful for checking sequential.

Speaker 1

Output ah right, controlling the order.

Speaker 2

Exactly and check same that matches patterns that must be on the exact same line. Brilliant for avoiding really long messy check lines when you need multiple things on one line keeps tests readable.

Speaker 1

Yeah, I can see that. Verbos ir needs concise checks. What if you want to ensure something isn't there or if the order doesn't matter?

Speaker 2

Good questions. For negative checks, there's check not. It asserts a pattern does not exist. Really handy for saying Okay, I expect why, but I definitely don't want to see X.

Speaker 1

Makes sense asserting the absence of something.

Speaker 2

And for when the order might change, maybe due to optimizations. Shuffling code around you use check DAG. That stands for a directed ecyclic graph, but here it means it allows matching texts and arbitrary orders. Super flexible for testing nondeterministic output.

Speaker 1

Wow, check DAG. That's really flexible. It seems like you can test the intent behind the code changes, not just the literal output strengths.

Speaker 2

That's exactly the point. It's about semantic checking, not just textual matching.

Speaker 1

So, speaking of describing intent and structure, compilers deal with incredibly complex structured data instruction sets, optimization rules. How does LLVM handle describing that efficiently? Is that table gen You've nailed it.

Speaker 2

Tablegen is the answer. There, it's a domain specific language a DSL ESL. Yeah. It originally started within LVM for describing things like process or instruction sets, the ISA, and other hardware details, but its use has just exploded.

Speaker 1

How so what else does it use for?

Speaker 2

Oh? Everything, managing Clang's command line options, defining complex optimization rules like the inst combine people optimizations. The book says it's basically for any tasks that involve non trivial static and structural data.

Speaker 1

So much broader than just hardware. Now it's a general tool for this kind of static data. Can you give us a quick feel for the syntax? How does it work?

Speaker 2

Sure? At its core, you define a class. Think of it like a C plus plus struct It defines a layout, fields and types. Then you use def to create an instance of that class called a record.

Speaker 1

Like creating an object from a class blueprint exactly.

Speaker 2

And you can override specific fields in that instance using the let keyword.

Speaker 1

Okay, and what about these bang operators I've heard about, dot AD, dot mole Ah, Yes.

Speaker 2

Those aren't run time functions. They're more like macros that get evaluated during build time buil tag. Yeah, so you can do simple computations right in the table gen file itself. The example given is dot mole kilogram one thousand to maybe convert units. It happens when table gen runs, not when the compiler runs.

Speaker 1

Later clever build time computation, or if you have lots of similar records, is there a shortcut?

Speaker 2

Yes, that's where multi class comes in. It's a way to define multiple records at once by factoring out common parameters like a template sort of. Yeah. The book uses an autopart and car example. You define a multi class for parts, then use defen to instantiate multiple cars, and it automatically generates all the individual part records like car one, fuel tank, carto, engine, et cetera from one definition.

Speaker 1

Very concise, nice as boilerplate right and complex relationships.

Speaker 2

Yeah, graphs for that. Tablegen has a specific DAG data type that lets you define directed cyclic graph instances explicitly, super important for things like instruction selection patterns or optimization rules where you have dependencies. You can even use tags like the upper term dollars to give parts of the daglogical names.

Speaker 1

A DAG type built in. Yeah, that's powerful, and the source uses this amazing analogy to make a concrete right a donut recipe it does.

Speaker 2

It's a brilliant example. The book uses a delicious doughnut recipe to show tablegen's power.

Speaker 1

How does that work? A doughnut recipe and a piler book.

Speaker 2

It defines unit classes like gramunit peb's peanut, then ingredient base records, and finally step records. These step records form a DAG representing the cooking actions. Makes this add that complete with ingredients and amounts. Wow, it's a perfect analogy because it takes this abstract idea of describing structured data and makes it totally tangible. You immediately see how it parallels describing instruction patterns or optimization steps.

Speaker 1

A donut recipe in compiler engineering. That's definitely an aha moment, makes total sense. So, okay, you've described your donut recipe or your instruction set in table gen. How do you actually use that data like print the recipe?

Speaker 2

Right? For that, you need a custom table gen back end? Now important distinction. This isn't an LVM back end like for generating machine code.

Speaker 1

Different kind of back end, totally different.

Speaker 2

A table gen back end is a piece of code, usually C plus A that convert or transpiles table gen files into an arbitrary, textual.

Speaker 1

Content arbitrary, so anything pretty much.

Speaker 2

It could generate a C plus plus header file documentation or in the donut example, just plain text for the recipe you use C plus plus APIs provided by tablegen like recordkeeper dot get all the rive definitions to get all the defined steps and record dot get value restring to pull out specific values like ingredient names or amounts.

Speaker 1

So you turn the table genstructure into usable code or data.

Speaker 2

Exactly, you transform the structured description into whatever format you need downstream.

Speaker 1

That ability to generate code is huge. Yeah, it feels like that opens the door to extending client itself, maybe injecting custom logic into the frontend.

Speaker 2

It absolutely does. The front end is a prime place for customization. Think about the preprocessor, the very first stage handling macros includes you can customize that. Oh yeah, you can write custom Pragma handler extensions, so you can invent your own hashtag pragma directives. The book shows an example hashtag pragma macro or guard.

Speaker 1

What would that do?

Speaker 2

Well? When the preprocessor sees your prag your handler code runs. It can parse the Pragma arguments and even register something called PEP callbacks. Callbacks, yeah, pp callbats let you hook into various preprocessor events, so you can insert custom logic whenever a preprocessor event happens. In the example, a macroguard validator uses the macro defined callback to automatically check if arguments in certain macros are properly wrapped in parentheses.

Speaker 1

Wow, that's fine grain control. Right at the start. What of the driver, the thing that orchestrates GCC or Clang? Can you customize that too?

Speaker 2

You can? The driver is basically the dispatcher, right, it passes flags and manages the different compilation phases. And guess what Clang uses to define its driver flags.

Speaker 1

Let me guess tablechen.

Speaker 2

You got it, tablegen again. You can declare custom flags, even paired flags like tay flag and NAM flag, using things like the booleion f flag, multi class and table gen.

Speaker 1

So you define your flag in table gen and Klang understands it exactly.

Speaker 2

The source gives an example of a custom fuse simple log flag. Defining this in tablegen allows the driver to recognize it, and then your custom logic can make it implicitly include a specific header simplelog dot H and maybe define macros to control log levels all driven by that one flag.

Speaker 1

That's really neat centralized control via custom flag. But can you go even deeper, like fundamentally change how Clang interacts with the system's tools, make it output something totally different.

Speaker 2

You absolutely can using custom toolchains. The toolchain normally adapts Clang for different platforms like different ozes or architectures, but you can make it do completely customed things like what The book has this fantastic, almost wild example called the zipline toolchain. It's a demo obviously, but it shows the power zipline.

Speaker 1

What does it do?

Speaker 2

Instead of normal compilation, it uses Clang, but then it encodes the generated assembly code using base sixty four during the assembling phase.

Speaker 1

Base sixty four why, just to show it can.

Speaker 2

And then during the linking phase it packages those base sixty four files into a ZP archive.

Speaker 1

Okay, that is wild? How does it that? In?

Speaker 2

Through the tool chain definition, you override methods like ad Clang system include ARGs can add custom include paths. Build assembler gets overridden to call open cell base sixty four instead of the normal assembler, and build linker gets overridden to call zip or tar instead of the linker.

Speaker 1

Wow. So you're completely replacing standard build steps with custom commands.

Speaker 2

Exactly. It perfectly illustrates how deeply you can customize the entire pipeline if you need to.

Speaker 1

So if you thought compilers where a black box, definitely think again. We're not just peeking inside. We're fundamentally changing how they work, how they talk to the OS. That level of control it must open up amazing possibilities for optimization and analysis. Right.

Speaker 2

Absolutely, that's where the real power of LLVM shines. Sophisticated optimizations need deep program understanding, and this happens primarily in llvm.

Speaker 1

IR, right, the intermediate representation.

Speaker 2

Exactly, it's the target independent intermediate representation. It's the core of the entire LLVM framework where most analysis and transformation happens.

Speaker 1

And the mechan is and for doing these transformations is passes. Right, what's a pass and what's this new pass manager deal?

Speaker 2

Think of an LLVM pass as a module, a basic unit that performs certain actions against LLVMI are like one step on a factory assembly.

Speaker 1

Line, Okay, a modular step, right.

Speaker 2

And the new pass manager is a significant redesign compared to the older system. The book highlights it runs faster and generates results with better quality, partly due to a cleaner interface.

Speaker 1

Can you give an example of a simple pass.

Speaker 2

Sure the source shows a strict up pass. Its goal is simple, add the nolias attribute to function arguments that are pointers no alias.

Speaker 1

What does that tell the compiler?

Speaker 2

It's a powerful hint. It guarantees that pointer does an alias, meaning it doesn't point to the same memory location as any other pointer accessible in that scope. This lets the optimizer be much more aggressive, assuming less potential overlap, which can unlock significant speed ups.

Speaker 1

How does the pass know what other passes have done?

Speaker 2

Ah, that's key to the new manager. When you write a pass, you have to clear what analysis it preserves. You use preserved analyzes, so if your pass adds no alias, it might invalidate alias analysis results. You tell a manager, maybe AA manager results are no longer valid.

Speaker 1

So you explicitly state what your pass.

Speaker 2

Breaks, well, rather what it doesn't break. By default, it assumes you break everything you specify what's preserved. This avoids costly recomputation of analyzes that are still perfectly valid. It's like a librarian keeping track much more efficient.

Speaker 1

Makes sense. So passes are the workers, but they need information to do complex jobs, they need a brain, right is that the analysis manager?

Speaker 2

Precisely, you nailed it. Modern compiler optimizations can be complex. They require lots of information and often getting that information is expensive to evaluate.

Speaker 1

So the analysis manager helps with that.

Speaker 2

Yes, it handles all tasks related to program analysis. It runs the analysis passes, and crucially caches their results so they don't have to be rerun constantly.

Speaker 1

Can you give an example of an analysis?

Speaker 2

It might manage the source mentions a hal tantalizer project. Its goal is to find code that's unreachable because a special function like my halt gets called earlier.

Speaker 1

Okay, dead god detection.

Speaker 2

Sort of yeah. And to do this it relies on one of the fundamental analyzes. LVM provides the dominator tree.

Speaker 1

Or DT dominator tree. How does that help find unreachable code after my halt?

Speaker 2

Okay? So the dominator tree tells you control flow relationships. If basic block A dominates basic block B, it means every possible path to B must go through A first.

Speaker 1

Ah.

Speaker 2

I see, So if the block containing my halt dominates another block and my halt stops execution, then that dominated block is definitely unreachable. Dominator tree analysis computes this tree structure, and halt tantalizer just needs to query it.

Speaker 1

That's really clever. Leveraging fundamental graph analysis.

Speaker 2

Exactly and understanding these core analyzes like dominator trees is what lets you build much smarter, much more effective custom optimization or analysis passes. You're building on solid theory foundations.

Speaker 1

That's a powerful concept. But okay, even with great optimizations, things go wrong. You need to debug, diagnose issues, check run time behavior. What tools does LVM offer there?

Speaker 2

Right, optimization isn't everything. LVM has some essential support utilities for debugging your passes themselves. There's lvmd bug.

Speaker 1

How does that work?

Speaker 2

You sprinkle LLVMD e bug DDGSS calls in your passcode. These messages only get printed if you run the optimizer tool with the ededbug or dbug only your past name flag keeps your production builds clean, but gives you detailed logs when you need them.

Speaker 1

Nice conditional logging. What about tracking numbers like how many times an optimization fired?

Speaker 2

For that, you use the statistic macro. You just declare statistic counter name description and then increment counter name in your code. LVM automatically collects these organizes them and can print them out even in formats like.

Speaker 1

Chason Jason output. That's useful for automation.

Speaker 2

Very useful turns ad hoc counting into structured data for analysis. Let's you see if you're pass is actually doing what you thought or hitting unexpected bottlenecks.

Speaker 1

Okay, what if an optimization tries to do something but fails, like it wants to vectorize a loop but can't, how do you find out why?

Speaker 2

That's exactly what optimization remarks are for you use optimization remark emitter in your past to report why something happened or didn't.

Speaker 1

Happen, So notes from the optimizer pretty much.

Speaker 2

The book uses the example of loop invariant code motion LICM. If it fails to hoist an instruction out of a loop, it can emit a remark saying why maybe there's a potential side effect it couldn't ignore.

Speaker 1

You can see these remarks yes.

Speaker 2

And even better, there's a tool Optviewer dot kei that takes these remarks and generates a webpage. It highlights the relevant source code lines and shows the remarks right next to them. It's like a visual debugger for your optimizations.

Speaker 1

Wow from pinpointing buffer overflows, which we'll get to to visualizing optimization decisions. Yea LVM really does feel like it gives you X ray vision into your code.

Speaker 2

It's a good way to put it. And beyond these utilities there's the whole world of instrumentation.

Speaker 1

Instrumentation, you mean adding code to collect data at runtime.

Speaker 2

Exactly, inserting some probes into the code we are compiling in order to collect runtime information. Two massive areas here are sanitizers and profile guided optimization or PGO.

Speaker 1

Sanitizers sound like they're about fixing problems or finding them. Give us a dramatic example.

Speaker 2

Okay, address sanitizer classic example, buffer overflow dot C. Normally it might just crash cryptically or worse, seem to work but corrupt memory right hard to debug. But compile it with Clang dash sanitize address. Now when you run it and hit the overflow asan doesn't just crash. It prints a detailed report that points out the problematic area with high accuracy, right away tells you the exact line the variable everything.

Speaker 1

How does it do that?

Speaker 2

It works by having the compiler inserting a boundary check into the array index access in the LVMR. It adds runtime guards. I remember this one bug. We spent days stepping through GDB asan found it instantly on the Total game Changer.

Speaker 1

That's incredible. Are there other sanitizers custom ones?

Speaker 2

Yes? Many built in ones, and the source describes building a custom one loop counter sanitizer or LPC SAND.

Speaker 1

What does that do?

Speaker 2

It's designed to collect the exact trip count of every loop in a module. It does this by using an LVM pass to insert function calls like LPC sandst loopstart and LPC senate loop in into the IR around loops. These functions part of the compiler RT runtime library, then record the counts when the instrumented code runs.

Speaker 1

You build your own runtime analysis tools using the same infrastructure. Very cool. And the other big area.

Speaker 2

Was PGO profile guided optimization. The idea here is to use runtime information not just for debugging, but to enable more aggressive compiler optimizations.

Speaker 1

How does runtime info help optimize?

Speaker 2

It tells the compiler how the code is actually used, which paths are hot, which are cold, which branches are almost always taken. This lets it make smarter decisions about things like function in lining, block lege out register allocation optimizations that are risky without knowing typical behavior.

Speaker 1

So it learns from real usage. How do you get that usage data?

Speaker 2

Two main ways. Instrumentation based PGO adds counters directly into the code. It's very precise, but adds overhead during the profiling run. Sampling based PGO uses external tools like Linux perf to statistically sample the program counter during execution. Lower overhead but less precise data.

Speaker 1

Okay, so you gather the data than what.

Speaker 2

First, you compile with a flag like clang def profile generat for instrumentation. This creates a profiled data file when you run the program. Then you can use llvm def prof data to merge and maybe inspect this data. It can show you things like the execution frequency of all the enclosing basic.

Speaker 1

Blocks to see the hotspots exactly.

Speaker 2

Finally, you recompile your program, but this time with clang def profile use, feeding that profile data back into the.

Speaker 1

Compiler and the compiler uses it. Yes.

Speaker 2

Yeah, the LVMIR actually gets annotated with metadat. You'll see things like dot prop point seven to one or dot prof point seven to two attached to instructions or branches. These represent the collected frequencies and probabilities directly guiding the optimization passes, the compiler literally learns from the profile that.

Speaker 1

Closes the loop nicely, compiler learns from runtime. Okay, wow, we have covered a ton of ground today.

Speaker 2

We really have.

Speaker 1

From you know, speeding up those massive builds, mastering testing with lat and file check's crazy pattern matching.

Speaker 2

To crafting custom dsls using table gen even from doughnut recipes right.

Speaker 1

And extending Klang's front end building whole custom tool chains, then diving deep into LLVMR, the new pass manager analysis like dominator trees, and.

Speaker 2

Finally using runtime techniques like sanitizers for correctness and PGO for performance, plus all those handy debug utilities.

Speaker 1

It's been a whirlwind tour. But the key takeaway, I think is that this deep dive using a guide like the source book really offers you a shortcut. Doesn't it a way to get a handle on LVM's huge, huge capabilities.

Speaker 2

Absolutely, it turns these conmplex compiler engineering problems into challenges you can actually tackle. It gives you that mental framework, the specific tools, the techniques to really engage with LVM effectively.

Speaker 1

We've seen how LVM lets you dissect code, rebuild it, get incredible control, even less the compiler learn from how programs run in the real world. With PGO, it's quite powerful, which leads to a final thought. If the compilation process itself can learn and adapt based on actual usage, what fundamental assumptions that we currently make about software design and development might we need to reconsider next? Something to chew on.

Speaker 2

Definitely something to think about and if you want to dive deeper. The LVM community is very active. Check out the mailing lists on lists at LVM dot org, or the discourse forums at LVM dot discourse dot group, or.

Speaker 1

Even attended LVM Developers Meeting LVM dot org DDVMTG. Lots of ways to keep learning and engage.

Speaker 2

The resources out there once you know where to look, are invaluable

Transcript source: Provided by creator in RSS feed: download file
For the best experience, listen in Metacast app for iOS or Android