5 ms·
I worked with Ada a lot during the 90s (and even maintained the Linux port of GNAT early on). This document does not address one thing that I am curious about.
by mcculley 5y ago
I worked with Ada a lot during the 90s (and even maintained the Linux port of GNAT early on). This document does not address one thing that I am curious about. What does the modern, idiomatic Ada programmer do about memory management? Are library implementers still required to use Unchecked_Deallocation? I don't see how a language where Unchecked_Deallocation is used can be considered safe at a time when we have modern choices with automatic memory management.
Edit: I guess another way to put this is, "Can a non-trivial project be developed in modern Ada without using Unchecked_Dealloation?" Because I don't see how I can consider a library safe if it is allowed to call Unchecked_Deallocation.
- zetalyrae 5y agoOff the top of my head there are two ways: - Lexically scoped access types have storage pools that are destroyed when the type goes out of scope. This is similar to region-based memory management as in Cyclone[0]. - Limited controlled types are a lot like linear types and prevent unwanted aliasing and ensure deallocation happens correctly, as described in this paper[1]. [0]: https://en.m.wikipedia.org/wiki/Cyclone_(programming_language) https://en.m.wikipedia.org/wiki/Cyclone_(programming_languag... [1]: https://dl.acm.org/doi/10.1145/165354.165362 https://dl.acm.org/doi/10.1145/165354.165362
- mcculley 5y agoI think I understand these. I think these solutions work where it is very clear what the scope of object should be. But what about cases in large projects with many developers where a library/package allocates an object on the heap and passes it back to a caller? Sometimes it is not clear who is responsible for deallocating the object. This is the challenge we ran into building large projects in C, C++, and Ada. These days, I use Java, C#, Rust, or Go, because I know there will be no dangling pointers.
- zetalyrae 5y ago>a library/package allocates an object on the heap and passes it back to a caller Limited controlled types handle this case. Limited types prevent assignment (i.e. aliasing). Controlled types introduce destructors so everything is closed even when an exception is thrown.
- mcculley 5y agoI see how limited controlled types allow for reference counting. I don't see how they can be used to implement circular graphs and situations where it is not clear who needs an object to continue to exist.
- nine_k 5y agoMaybe the semantic problem you are trying to solve should be not solved but avoided. Much like "spaghetti code" is not only considered poor style but isn't even supported by modern languages, "spaghetti data" should also be considered a bad pattern, which more advanced languages force you to avoid.
- zetalyrae 5y agoI haven't written enough Ada to know the answer (I've only researched the memory management scheme as part of implementing a language with compile-time ownership tracking). I think the Ada answer would be to keep everything that has a lifetime, anything that's a resource, in its own module behind a strict limited controlled interface. But that's a very vague answer.
- OneWingedShark 5y agoIIRC a circular graph would typically be done via pool-specific access types. -- Forward declaration. Type Element(<>); -- Assuming there's a Graph.Pool implementation of the base Storage_Pool object. Type Pointer is access Element with Storage_Pool => Graph.Pool; Subtype Handle is not null Pointer; Type Children is array(Positive range <>) of Handle; Type Element(Parent : Handle; Child_Count: Natural) is record Data : Integer; -- Or whatever your actual data would be. Link : Children( 1..Child_Count ); end record;
- pyjarrett 5y ago> What does the modern, idiomatic Ada programmer do about memory management? Author here. I've actually not had to do any heavy direct memory management yet. There's an RAII-based (i.e. Controlled Type) reference-counted pointer implementation in GNAT I've used and the container libraries is mature enough to avoid the issue by just using built-in data structures. A few other smart pointer implementations exists floating around in various libraries in Alire, I suspect there will be a default one will appear in Alire at some point which parallels Rust's Arc, Rc and Box, or C++'s std::shared_ptr, std::weak_ptr (for breaking cycles) and std::unique_ptr. I have a partial implementation myself, but I've been working on other things.
- MaxBarraclough 5y agoYou might be interested in this thread from a year ago, where we discussed memory management in Ada. [0] The Ada folks sometimes seem a bit quiet on memory-management questions, a bit like the way Forth folks rarely talk about the real-world performance of Forth interpreters. On the plus side, borrower-checking is coming to SPARK. [1][2] [0] https://news.ycombinator.com/item?id=24361992 https://news.ycombinator.com/item?id=24361992 [1] https://www.adacore.com/papers/safe-dynamic-memory-management-in-ada-and-spark https://www.adacore.com/papers/safe-dynamic-memory-managemen... [2] https://arxiv.org/abs/1805.05576 https://arxiv.org/abs/1805.05576
- InfiniteRand 5y agoOne thing to keep in mind in Ada is the "typical" Ada programmer lives in embedded world, where memory needs to be deterministic. It's a very different perspective from say "desktop-application" world where dynamically allocating memory is just a natural part of programming.
- mcculley 5y agoI get that. I just keep seeing Ada recommended for projects other than embedded systems and I don’t see how it can work.
- MaxBarraclough 5y agoI'm not especially knowledgeable about Ada, but looking at the docs for GtkAda, it seems they're able to do reference-counting with good ergonomics. https://docs.adacore.com/live/wave/gtkada/html/gtkada_ug/memory.html https://docs.adacore.com/live/wave/gtkada/html/gtkada_ug/mem...
- pyjarrett 5y agoI found it a lot easier than I thought it would be. Ada has RAII (controlled types) which you can use for reference counting and resource management like in C++. Septum uses a lot of dynamically allocated memory, but it's all hidden by smart pointers or Unbounded_String (built-in reference-counted COW strings). I use it routinely to search codebases in the tens of millions of lines and it's fast enough right now (single second search times) due to task parallelism I did in Ada that I haven't even bothering to tune it or do any fancy indexing yet.
- zmower 5y agoWell, we do have a good set of generic containers now. Access types and Unchecked_Conversion have always been a code-smell IMO. And Ada is still foremost a realtime language so garbage collection just creates scheduling problems.
- mcculley 5y agoI think that is a fine answer for limiting the applicability of Ada. I’m confused where people suggest it for bigger systems. I was on a team using it for a large soft real-time simulation. Memory management was a constant source of errors.
- zmower 5y agoI'm confused when people don't use it for bigger systems. I like that the compiler detects most of my errors before I run the program.
- mcculley 5y agoMy experience with bigger systems is that when you have 100 developers passing references around it becomes hard to manage who is responsible for deallocation of an object. This leads to dangling pointers and debugging. Going to languages with automatic memory management made such projects a lot more reliable.
- zozbot234 5y agoRust surfaces this information as part of the type and lifetime system. There's no ambiguity there: the part of the program that "owns" any data object will automatically deallocate the object if it's done with it and has not transferred ownership elsewhere. This works exactly like the usual C++ RAII, but it's generalized to the whole language. Even the standard .drop() operation follows these semantics.
- joakimds 5y agoThanks for clarifying how the dangling pointers may arise. Not everyone agrees with me, but these are my thoughts/recommendations when using Ada. Which thread/task that has ownership of a variable is paramount. Whenever one defines a variable it must be crystal clear which thread/task that owns it, for example has the right the read or write a value to the variable. What I recommend is the Actor Model (https://en.wikipedia.org/wiki/Actor_model https://en.wikipedia.org/wiki/Actor_model). Synchronization between two tasks can either be through shared variables or message passing. Last time I checked Academia is inconclusive as to what is the best (least error-prone) way for threads/tasks to communicate. What seems the simplest to me is message passing. 10 years ago, first time I heard of the Actor Model and message passing is Erlang and it's a language where these ideas are fundamental. So a task owns a variable. If another task wishes to change the value of that variable it must send a message to the owning task and request it to change the value. If another task wishes to know the value it must ask the owning task what the value is. Since the time I heard of Erlang, other languages like Rust and the Pony language has picked up on this too. Rust has taken this further by making it possible for one task to temporarily borrow ownership to another task and it is checked by the borrow-checker. To implement the Actor Model in Ada one puts all variables in the body of the tasks that are in the application. It makes them not visible from other tasks. So what you need to keep in mind when developing is for a task to never send an access-to-object type variable to another task. If there is a need to do that you need to use Ada/SPARK or Rust to get the proper ownership checking done. Btw, Codepeer (static code analysis tool for Ada) finds race-conditions, has deadlock detection, and warns if there are variables that may be read or written to by more than one task. If one sticks to vanilla Ada (not SPARK) one could develop an application based on libadalang that parses all the Ada source code and checks that all task entries have input arguments that do not contain any access-to-object types (to find instances where a developer has sent an access-to-object variable to another task by mistake). Such a tool does not exist but libadalang exists to allow the creation of custom rules checking on one's Ada code.
- guerby 5y ago"Are library implementers still required to use Unchecked_Deallocation?" Unchecked_Deallocation has more in it than just returning storage to the system, it also triggers Finalization: http://www.ada-auth.org/standards/rm12_w_tc1/html/RM-13-11-2.html http://www.ada-auth.org/standards/rm12_w_tc1/html/RM-13-11-2... "when X is not equal to null first performs finalization of the object designated by X (and any coextensions of the object ...)" So even if your compiler target has automatic memory management (eg: JVM) you will still use Unchecked_Deallocation if you want to control when Finalization occurs.
- geofft 5y agoNot having written Ada, but having seen many HN naysayers saying "Why do we need Rust when Ada can do this" and having tried to come up with a well-researched rebuttal, my sense is that there are three options: 1. Your project is static enough that it doesn't need dynamic memory allocation. The control system for a modern passenger jet is a pretty complex project, but you can allocate an object per engine, an object per flap, a struct per landing gear, an object per pilot display, etc. at design time. Even the navigational computers on these aircraft, which take in unbounded structured data about airports and navigational points, often have hard-coded limits. Quoting https://www.mitre.org/sites/default/files/pdf/12_1324.pdf https://www.mitre.org/sites/default/files/pdf/12_1324.pdf : > one widely used FMC [flight management computer] model ... has issues at airports with over 100 arrival and departure procedures. Some airport examples where 100 arrival and departure procedures are exceeded include Cairo, Amsterdam, Madrid, Paris (Le Bourget, Orly and Charles de Gaulle), Mumbai, New Delhi and Beijing. A FMC service bulletin issued states that the aircraft may lose FMC applications in flight with over 100 procedures and flight plan up-linking. Further, another manufacturer has a FMC model with a limit of 99 total procedures and a limit of 8 waypoints per procedure. A third FMC manufacturer has a model with a limit on the amount of arrivals, departure and approaches as shown below: Early models – limit of 70 departures, 70 arrivals, and 29 approaches at an airport. Later models – limit of 130 departures, 130 arrivals, and 39 approaches at an airport. Counterintutitively, this is culturally acceptable for such "high assurance" applications, but WhatsApp gets confusion from the press when it increases the maximum number of participants in a group chat to the "oddly specific number" of 256 (https://i.redd.it/gk7dicv6hsvy.jpg https://i.redd.it/gk7dicv6hsvy.jpg). 2. You use a third-party GC/refcounting library that calls Unchecked_Deallocation internally. 3. There's a 2018 proposal (linked in another reply) for "ownership types", which specifically references Rust's lifetime model as an inspiration. Or, in other words, if your goal is to write a (say) safe implementation of the DOM that can render real-world web pages while efficiently using memory on a general-purpose computer, Ada is probably the wrong tool for the job. Which is fine! It's a great tool for other jobs.
- pyjarrett 5y ago> if your goal is to write a (say) safe implementation of the DOM that can render real-world web pages while efficiently using memory on a general-purpose computer, Ada is probably the wrong tool for the job. Author here. Ada is actually perfect for the use case you listed. As a C++ programmer focusing on performance, the applicability of Ada to "modern problems" was one of the reasons I was playing around with it. Between customizable "storage pools" (Ada's allocators), the easy of interfacing with C, control of type alignment and layout, compiler intrinsics, built-in concurrency types (protected objects for coordinating access, and tasks for splitting work) and a bunch of other things, I have all the tools I need to do this. It's pretty close to the C++ feature set with the face of Pascal, and C++ programmers should feel comfortable working in Ada after only a few months.
- touisteur 5y agoYes you can and most of people in the embedded world doing Ada following mostly the same guidelines. 1- Don't use pointers. Or use not-null pointers. But preferably don't use them. With in/out semantics you don't need passing pointers or references around, and the addition of official data structures in Ada 2005 make most uses of pointers obsolete. 2- use controlled types when you're allocating to free soon. Limited_Controlled_Type are even safer if you're prepared to deal with a limited type :-) 3- recently, use SPARK, with safe-memory semantics. Hard work, but so is pleasing the borrow-checker... edit Of course one of the impetuses (impetii) of manual memory management is having functions return objects of unknown size (at call site). In Ada this isn't a necessity, at least with GNAT's secondary stack.edit-end I've programmed Ada almost every day of the work week for 13 years now, I can count on one hand the problems related to memory management, and opening the code you knew you were in for a treat. 'Smells like tiny cache syndrome' just remove those 90's blotches from our humongous codebases when we face them. I'm almost willing to make access types forbidden except in pools (so, not null he), with an open door policy 'explain to me so I can update our coding rules with that case if we find it worth the risk'. Forced software design review by linter.
- OneWingedShark 5y ago>I guess another way to put this is, "Can a non-trivial project be developed in modern Ada without using Unchecked_Dealloation?" Because I don't see how I can consider a library safe if it is allowed to call Unchecked_Deallocation. Yes, there are very few times I've had to manually deallocate; there's a really good video on memory-management with Ada: http://video.fosdem.org/2016/aw1124/memory-management-with-ada-2012.webm http://video.fosdem.org/2016/aw1124/memory-management-with-a...