[Git][ghc/ghc][wip/unload-strategy] Introduce -funload-strategy
Zubin pushed to branch wip/unload-strategy at Glasgow Haskell Compiler / GHC Commits: 79f3073b by Zubin Duggal at 2026-08-25T13:17:55+05:30 Introduce -funload-strategy When unloading object code, we have a choice to make. Do we call purgeObj or unloadObj? purgeObj clears the symbol tables associated with an object, so that future objects can't link against it, but the object stays in memory unloadObj does the above, but it also marks the object as needing to be unloaded, so at some point in a future GC, the RTS may notice that it is no longer used, and if so, unload it entirely, freeing up the memory. Ideally we would always unload, but a number of bugs with the implementation of unloadObj mean that it is fragile on many platforms. This is documented in Note [Unloading vs purging objects]. So on these platforms we purge instead. We introduce the -funload-strategy flag, so that users can opt into purging/unloading on platforms where we make the other choice by default. The distinction is moot when we are using the dynamic RTS, we don't do either then. Fixes #27741 - - - - - 13 changed files: - + changelog.d/unload-strategy - compiler/GHC.hs - compiler/GHC/Driver/Config/Interpreter.hs - compiler/GHC/Driver/DynFlags.hs - compiler/GHC/Driver/Session.hs - compiler/GHC/Linker/Loader.hs - compiler/GHC/Runtime/Interpreter.hs - compiler/GHC/Runtime/Interpreter/Init.hs - compiler/GHC/Runtime/Interpreter/Types.hs - docs/users_guide/ghci.rst - testsuite/tests/ghc-api/T27606/T27606c.hs - + testsuite/tests/ghc-api/T27606/T27606c_purge.stdout - testsuite/tests/ghc-api/T27606/all.T Changes: ===================================== changelog.d/unload-strategy ===================================== @@ -0,0 +1,16 @@ +section: linker +synopsis: Add -funload-strategy to choose between purging and unloading object code +issues: #27741 +mrs: !16583 + +description: { + When the interpreter drops object code it has loaded it can either + purge it, clearing its symbol table entries but leaving it in memory, + or unload it, additionally allowing a later garbage collection to + reclaim its memory. Unloading is preferable, but its implementation is + fragile on a number of platforms, so GHC purges on those instead: it + unloads on Linux other than ARM, and purges everywhere else. + ``-funload-strategy=unload`` and ``-funload-strategy=purge`` opt into + the other choice. The flag has no effect when the interpreter is + dynamically linked, as neither happens then. +} ===================================== compiler/GHC.hs ===================================== @@ -29,7 +29,7 @@ module GHC ( -- * Flags and settings DynFlags(..), GeneralFlag(..), Severity(..), Backend, gopt, ncgBackend, llvmBackend, viaCBackend, bytecodeBackend, interpreterBackend, noBackend, - GhcMode(..), GhcLink(..), + GhcMode(..), GhcLink(..), UnloadStrategy(..), parseDynamicFlags, parseTargetFiles, getSessionDynFlags, setTopSessionDynFlags, @@ -728,6 +728,12 @@ setTopSessionDynFlags dflags = do interp <- liftIO $ initInterpreter dflags tmpfs logger platform finder_cache unit_env interp_opts + case (hsc_interp hsc_env, unloadStrategy dflags, interp) of + (Nothing, Just _, Just i) | interpreterDynamic i -> + liftIO $ logInfo logger $ withPprStyle defaultUserStyle $ + text "warning: -funload-strategy is ignored with a dynamic interpreter" + _ -> return () + modifySession $ \h -> hscSetFlags dflags h{ hsc_IC = (hsc_IC h){ ic_dflags = dflags } , hsc_interp = hsc_interp h <|> interp ===================================== compiler/GHC/Driver/Config/Interpreter.hs ===================================== @@ -43,4 +43,5 @@ initInterpOpts dflags = do , interpLdConfig = configureLd dflags , interpCcConfig = configureCc dflags , interpExecutableLinkOpts = initExecutableLinkOpts dflags Dynamic + , interpUnloadStrategyFlag = unloadStrategy dflags } ===================================== compiler/GHC/Driver/DynFlags.hs ===================================== @@ -40,6 +40,7 @@ module GHC.Driver.DynFlags ( isPackageDbRef, Option(..), showOpt, DynLibLoader(..), + UnloadStrategy(..), positionIndependent, optimisationFlags, @@ -308,6 +309,7 @@ data DynFlags = DynFlags { outputHi :: Maybe String, dynOutputHi :: Maybe String, dynLibLoader :: DynLibLoader, + unloadStrategy :: Maybe UnloadStrategy, dynamicNow :: !Bool, -- ^ Indicate if we are now generating dynamic output -- because of -dynamic-too. This predicate is @@ -657,6 +659,7 @@ defaultDynFlags mySettings = outputHi = Nothing, dynOutputHi = Nothing, dynLibLoader = SystemDependent, + unloadStrategy = Nothing, dumpPrefix = "non-module.", dumpPrefixForce = Nothing, ldInputs = [], @@ -965,6 +968,11 @@ data DynLibLoader | SystemDependent deriving Eq +data UnloadStrategy + = UnloadStrategyUnload + | UnloadStrategyPurge + deriving Eq + data RtsOptsEnabled = RtsOptsNone | RtsOptsIgnore | RtsOptsIgnoreAll | RtsOptsSafeOnly | RtsOptsAll ===================================== compiler/GHC/Driver/Session.hs ===================================== @@ -55,6 +55,7 @@ module GHC.Driver.Session ( PackageDBFlag(..), PkgDbRef(..), Option(..), showOpt, DynLibLoader(..), + UnloadStrategy(..), fFlags, fLangFlags, xFlags, wWarningFlags, makeDynFlagsConsistent, @@ -725,6 +726,12 @@ parseDynLibLoaderMode f d = ("sysdep", "") -> d { dynLibLoader = SystemDependent } _ -> throwGhcException (CmdLineError ("Unknown dynlib loader: " ++ f)) +parseUnloadStrategy :: String -> DynFlags -> DynFlags +parseUnloadStrategy f d = case f of + "unload" -> d { unloadStrategy = Just UnloadStrategyUnload } + "purge" -> d { unloadStrategy = Just UnloadStrategyPurge } + _ -> throwGhcException (CmdLineError ("Unknown unload strategy: " ++ f)) + setDumpPrefixForce f d = d { dumpPrefixForce = f} -- XXX HACK: Prelude> words "'does not' work" ===> ["'does","not'","work"] @@ -1892,6 +1899,8 @@ dynamic_flags_deps = [ (intSuffix (\n d -> d {maxForcedSpecArgs = n})) , make_ord_flag defGhciFlag "fghci-hist-size" (intSuffix (\n d -> d {ghciHistSize = n})) + , make_ord_flag defFlag "funload-strategy" + (hasArg parseUnloadStrategy) -- wasm ghci browser mode , make_ord_flag defGhciFlag "fghci-browser-host" ===================================== compiler/GHC/Linker/Loader.hs ===================================== @@ -111,6 +111,7 @@ import GHC.Linker.Types import Control.Monad import Data.Array +import Data.Containers.ListUtils (nubOrd) import Data.ByteString (ByteString) import qualified Data.Set as Set import Data.Char (isSpace) @@ -898,8 +899,9 @@ dropModules interp mods pls = do } } - mapM_ (purgeLinkableObjs interp) victim_usages - when (any (not . null . linkableUsageObjs) victim_usages) $ + let victim_objs = nubOrd (concatMap linkableUsageObjs victim_usages) + dropLinkableObjs interp victim_objs + when (not (null victim_objs)) $ purgeLookupSymbolCache interp mapM_ (removeSptEntry interp) @@ -914,31 +916,14 @@ dropModules interp mods pls = do modifyHomePackageBytecodeState (bco_loader_state pls) drop_bytecode_state } --- | Purge the symbols of a dropped module's objects. We don't unload --- them, because unloading is not well supported. --- See Note [Automatically reloading stale linkables] --- See Note [Unloading vs purging objects] in GHC.Runtime.Interpreter -purgeLinkableObjs :: Interp -> LinkableUsage -> IO () -purgeLinkableObjs interp lnk - | interpreterDynamic interp = return () - | otherwise - = mapM_ (purgeObj interp) (linkableUsageObjs lnk) - -- See Note [Unloading vs purging objects] in GHC.Runtime.Interpreter -unloadLinkableObjs :: Interp -> LinkableUsage -> IO () -unloadLinkableObjs interp lnk +dropLinkableObjs :: Interp -> [FilePath] -> IO () +dropLinkableObjs interp objs | interpreterDynamic interp = return () - -- We don't do any cleanup when linking objects with the - -- dynamic linker. Doing so introduces extra complexity for - -- not much benefit. | otherwise - = mapM_ (unloadObj interp) (linkableUsageObjs lnk) - -- The components of a BCO linkable may contain - -- dot-o files (generated from C stubs). - -- - -- But the BCO parts can be unlinked just by - -- letting go of them (plus of course depopulating - -- the symbol table which is done in the main body) + = case interpUnloadStrategy interp of + UnloadStrategyUnload -> mapM_ (unloadObj interp) objs + UnloadStrategyPurge -> mapM_ (purgeObj interp) objs -- | Load a linkable from a module, and add all the names from the linkable into the -- closure environment. @@ -1354,12 +1339,13 @@ unload_wkr interp pls@LoaderState{..} = do -- testsuite/ghci can detect space leaks here. let linkables_to_unload = moduleEnvElts objs_loaded ++ moduleEnvElts bcos_loaded + objs_to_unload = nubOrd (concatMap linkableUsageObjs linkables_to_unload) - mapM_ (unloadLinkableObjs interp) linkables_to_unload + dropLinkableObjs interp objs_to_unload -- If we unloaded any object files at all, we need to purge the cache -- of lookupSymbol results. - when (not (null (filter (not . null . linkableUsageObjs) linkables_to_unload))) $ + when (not (null objs_to_unload)) $ purgeLookupSymbolCache interp mapM_ (removeSptEntry interp) (concat (moduleEnvElts loaded_spt_keys)) ===================================== compiler/GHC/Runtime/Interpreter.hs ===================================== @@ -601,24 +601,33 @@ unloadObj interp path = do {- Note [Unloading vs purging objects] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -unloadObj removes the object's symbols and frees its memory. The memory -is only freed at a major GC, once nothing references the object. -purgeObj removes the symbols and never frees the memory. - -We only unloadObj in unload, which the driver calls before a -compilation sweep, when everything is unloaded together. We purgeObj -when dropModules replaces or removes single modules, because unloading -is not well supported on many platforms/configurations. Purging is -enough for correctness: new lookups find the replacement's symbols, -and values built by the old code and computations still using it keep -working. - -With a dynamic interpreter there is nothing to purge. Objects are -linked into temporary shared libraries and their symbols are found by -searching the loaded libraries, not in the linker's symbol table. -Dropping a module flushes the symbol cache, and the replacement is -loaded as a new library, so lookups find the replacement first and the -old library stays loaded. This behaves like purging. +There are two ways to drop objects: + +- unloadObj removes the object's symbols and eventually the RTS may decide to free its memory. +- purgeObj removes the symbols and never frees the memory. + +purgeObj is enough for correctness, but leaks memory. unloadObj can be finnicky on certain +platforms and/or may not be implemented correctly. + +We use the unload strategy given by -funload-strategy. +The strategy we use by default depends on platform calculus, given the +bugs that apply to each platform. We try to unload where we don't know of +any bugs affecting correctness: + +- Linux: mostly unload. + - i386/x86_64: unload + - AArch64: We unload, but perhaps we should purge instead because of #24170. + - 32-bit ARM: purge. Unloading is broken (#21991). +- FreeBSD: purge. Unloading is not implemented (#25491). +- Darwin: purge. The process crashes at exit if an unloaded object + had C finalizers (#27616). +- Windows: purge. Unloading code that is still in use is fragile + (#20852). +- Purge as a fallback for everything else + +With a dynamic interpreter we never purge, or unload: +Dynamic objects are linked into temporary shared libraries, and if a library is +reloaded, then it shadows over the old one. -} -- | Purge an object's symbols. ===================================== compiler/GHC/Runtime/Interpreter/Init.hs ===================================== @@ -57,6 +57,7 @@ data InterpOpts = InterpOpts , interpBrowserPlaywrightBrowserType :: Maybe String , interpBrowserPlaywrightLaunchOpts :: Maybe String , interpExecutableLinkOpts :: ExecutableLinkOpts + , interpUnloadStrategyFlag :: Maybe UnloadStrategy } -- | Initialize code interpreter @@ -87,6 +88,14 @@ initInterpreter dflags tmpfs logger platform finder_cache unit_env opts = do | otherwise = Just (objectSuf dflags, waysTag (WayDyn `addWay` target_full_ways) ++ "_o") + -- See Note [Unloading vs purging objects] in GHC.Runtime.Interpreter + let unload_strategy = case interpUnloadStrategyFlag opts of + Just s -> s + Nothing -> case (platformOS platform, platformArch platform) of + (OSLinux, ArchARM {}) -> UnloadStrategyPurge + (OSLinux, _) -> UnloadStrategyUnload + _ -> UnloadStrategyPurge + -- see Note [Target code interpreter] if #if !defined(wasm32_HOST_ARCH) @@ -116,7 +125,7 @@ initInterpreter dflags tmpfs logger platform finder_cache unit_env opts = do , wasmInterpHsSoSuffix = way_tag ++ dynLibSuffix (interpNameVer opts) , wasmInterpUnitState = ue_homeUnitState unit_env } - pure $ Just $ Interp (ExternalInterp $ ExtWasm $ ExtInterpState cfg s) loader lookup_cache fs_cache wasm_obj_suffix + pure $ Just $ Interp (ExternalInterp $ ExtWasm $ ExtInterpState cfg s) loader lookup_cache fs_cache wasm_obj_suffix unload_strategy #endif -- JavaScript interpreter @@ -135,7 +144,7 @@ initInterpreter dflags tmpfs logger platform finder_cache unit_env opts = do , jsInterpFinderOpts = interpFinderOpts opts , jsInterpFinderCache = finder_cache } - return (Just (Interp (ExternalInterp (ExtJS (ExtInterpState cfg s))) loader lookup_cache fs_cache Nothing)) + return (Just (Interp (ExternalInterp (ExtJS (ExtInterpState cfg s))) loader lookup_cache fs_cache Nothing unload_strategy)) -- external interpreter | interpExternal opts @@ -162,7 +171,7 @@ initInterpreter dflags tmpfs logger platform finder_cache unit_env opts = do } s <- liftIO $ newMVar InterpPending loader <- liftIO Loader.uninitializedLoader - return (Just (Interp (ExternalInterp (ExtIServ (ExtInterpState conf s))) loader lookup_cache fs_cache Nothing)) + return (Just (Interp (ExternalInterp (ExtIServ (ExtInterpState conf s))) loader lookup_cache fs_cache Nothing unload_strategy)) -- Internal interpreter | otherwise @@ -170,7 +179,7 @@ initInterpreter dflags tmpfs logger platform finder_cache unit_env opts = do #if defined(HAVE_INTERNAL_INTERPRETER) do loader <- liftIO Loader.uninitializedLoader - return (Just (Interp InternalInterp loader lookup_cache fs_cache internal_obj_suffix)) + return (Just (Interp InternalInterp loader lookup_cache fs_cache internal_obj_suffix unload_strategy)) #else return Nothing #endif ===================================== compiler/GHC/Runtime/Interpreter/Types.hs ===================================== @@ -52,6 +52,7 @@ import GHC.Platform #if defined(HAVE_INTERNAL_INTERPRETER) import GHC.Platform.Ways #endif +import GHC.Driver.DynFlags (UnloadStrategy) import GHC.Utils.TmpFs import GHC.Utils.Logger import GHC.Unit.Env @@ -83,6 +84,8 @@ data Interp = Interp , interpObjSuffix :: !(Maybe (String, String)) -- ^ @(from, to)@ object suffixes to swap when the interpreter cannot -- load objects built the target's way + + , interpUnloadStrategy :: !UnloadStrategy } data InterpInstance ===================================== docs/users_guide/ghci.rst ===================================== @@ -3554,6 +3554,32 @@ breakpoints in object-code modules, for example. Only the exports of an object-code module will be visible in GHCi, rather than all top-level bindings as in interpreted modules. +.. ghc-flag:: -funload-strategy=⟨strategy⟩ + :shortdesc: Whether to ``purge`` or ``unload`` object code that the + interpreter drops. + :type: dynamic + :category: linking + + :since: 10.2.1 + + When the interpreter drops object code it has loaded, it can either + *purge* it or *unload* it. + + Purging clears the symbol table entries the object contributed, so + that nothing linked afterwards can refer to it, but the object stays + in memory. Unloading does the same, and additionally marks the object + as no longer needed, so that a later garbage collection may notice + that nothing refers to it and reclaim its memory. + + Unloading is the better choice where it works, but its implementation + is fragile on a number of platforms, so GHC purges on those instead. + By default GHC unloads on Linux other than ARM, and purges everywhere + else. This flag opts into the other choice. + + The distinction is moot when the interpreter is dynamically linked, + as neither purging nor unloading happens then. GHC warns that the + flag is ignored in that case. + .. _external-interpreter: Running the interpreter in a separate process ===================================== testsuite/tests/ghc-api/T27606/T27606c.hs ===================================== @@ -24,11 +24,16 @@ import Unsafe.Coerce (unsafeCoerce) main :: IO () main = do - [libdir] <- getArgs + libdir:rest <- getArgs + let strat = case rest of + [] -> Nothing + ["purge"] -> Just UnloadStrategyPurge + ["unload"] -> Just UnloadStrategyUnload + _ -> error "usage: T27606c <libdir> [purge|unload]" writeA 1 writeC runGhc (Just libdir) $ do - setupSession ["B.hs", "C.hs"] + setupSession strat ["B.hs", "C.hs"] _ <- load LoadAllTargets setContext [ IIDecl (simpleImportDecl (mkModuleName "Prelude")) , IIDecl (simpleImportDecl (mkModuleName "A")) @@ -61,10 +66,10 @@ writeC = writeFile "C.hs" $ unlines , "c = unsafePerformIO (appendFile \"c.log\" \"x\" >> pure 100)" ] -setupSession :: [String] -> Ghc () -setupSession targets = do +setupSession :: Maybe UnloadStrategy -> [String] -> Ghc () +setupSession strat targets = do df <- getSessionDynFlags - _ <- setSessionDynFlags df { ghcLink = LinkInMemory } + _ <- setSessionDynFlags df { ghcLink = LinkInMemory, unloadStrategy = strat } ts <- mapM (\t -> guessTarget t Nothing Nothing) targets setTargets ts @@ -74,7 +79,7 @@ setupSession targets = do compileA :: String -> NameCache -> IO HomeModInfo compileA libdir nc = runGhc (Just libdir) $ do getSession >>= \h -> setSession h { hsc_NC = nc } - setupSession ["A.hs"] + setupSession Nothing ["A.hs"] ok <- load LoadAllTargets when (failed ok) $ error "compileA: load failed" hsc <- getSession ===================================== testsuite/tests/ghc-api/T27606/T27606c_purge.stdout ===================================== @@ -0,0 +1,5 @@ +102 +102 +104 +104 +1 ===================================== testsuite/tests/ghc-api/T27606/all.T ===================================== @@ -25,6 +25,15 @@ test('T27606c', compile_and_run, ['-package ghc']) +test('T27606c_purge', + [extra_run_opts(f'"{config.libdir}" purge'), + extra_files(['T27606c.hs', 'B.hs']), + req_rts_linker, + when(config.ghc_dynamic, skip), + when(arch('wasm32'), skip)], + multimod_compile_and_run, + ['T27606c', '-package ghc']) + test('T27606d', [extra_run_opts(f'"{config.libdir}"'), req_rts_linker, View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/79f3073b4f9dd3e58959a6643e506c92... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/79f3073b4f9dd3e58959a6643e506c92... You're receiving this email because of your account on gitlab.haskell.org. Manage all notifications: https://gitlab.haskell.org/-/profile/notifications | Help: https://gitlab.haskell.org/help
participants (1)
-
Zubin (@wz1000)