diff --git a/CHANGES.txt b/CHANGES.txt
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,8 @@
 Changelog for Shake (* = breaking change)
 
+0.17.3, released 2018-12-04
+    #632, remove O(n^2) behaviour when constructing user rules
+    In diagnostic mode, show the number of actions/rules/user rules
 0.17.2, released 2018-11-29
     Make needHasChanged work even between runs
 0.17.1, released 2018-11-14
diff --git a/shake.cabal b/shake.cabal
--- a/shake.cabal
+++ b/shake.cabal
@@ -1,7 +1,7 @@
 cabal-version:      >= 1.18
 build-type:         Simple
 name:               shake
-version:            0.17.2
+version:            0.17.3
 license:            BSD3
 license-file:       LICENSE
 category:           Development, Shake
diff --git a/src/Development/Shake/Internal/Args.hs b/src/Development/Shake/Internal/Args.hs
--- a/src/Development/Shake/Internal/Args.hs
+++ b/src/Development/Shake/Internal/Args.hs
@@ -100,6 +100,7 @@
 --   be @[]@.
 --
 -- * @argValues@ is a list of non-flag arguments, which are often treated as files and passed to 'want'.
+--   If arguments are specified then typically the 'want' calls from the rules are discarded using 'withoutActions'.
 --
 -- * @result@ should produce a 'Nothing' to indicate that no building needs to take place, or a 'Just'
 --   providing the rules that should be used.
@@ -113,12 +114,14 @@
 -- flags = [Option \"\" [\"distcc\"] (NoArg $ Right DistCC) \"Run distributed.\"]
 --
 -- main = 'shakeArgsWith' 'shakeOptions' flags $ \\flags targets -> return $ Just $ do
---     if null targets then 'want' [\"result.exe\"] else 'want' targets
 --     let compiler = if DistCC \`elem\` flags then \"distcc\" else \"gcc\"
---     \"*.o\" '%>' \\out -> do
---         'need' ...
---         'cmd' compiler ...
---     ...
+--     let rules = do
+--         \"*.o\" '%>' \\out -> do
+--             'need' ...
+--             'cmd' compiler ...
+--         'want' [\"target.exe\"]
+--         ...
+--     if null targets then rules else 'want' targets >> 'withoutActions' rules
 -- @
 --
 --   Now you can pass @--distcc@ to use the @distcc@ compiler.
diff --git a/src/Development/Shake/Internal/Core/Action.hs b/src/Development/Shake/Internal/Core/Action.hs
--- a/src/Development/Shake/Internal/Core/Action.hs
+++ b/src/Development/Shake/Internal/Core/Action.hs
@@ -354,7 +354,7 @@
 historyDisable = Action $ modifyRW $ \s -> s{localHistory = False}
 
 
--- | This rule the following files, in addition to any defined by its target.
+-- | This rule builds the following files, in addition to any defined by its target.
 --   At the end of the rule these files must have been written.
 produces :: [FilePath] -> Action ()
 produces xs = Action $ modifyRW $ \s -> s{localProduces = map (True,) (reverse xs) ++ localProduces s}
diff --git a/src/Development/Shake/Internal/Core/Rules.hs b/src/Development/Shake/Internal/Core/Rules.hs
--- a/src/Development/Shake/Internal/Core/Rules.hs
+++ b/src/Development/Shake/Internal/Core/Rules.hs
@@ -115,8 +115,8 @@
 newRules :: SRules -> Rules ()
 newRules x = Rules $ liftIO . flip modifyIORef' (<> x) =<< asks snd
 
-modifyRules :: (SRules -> SRules) -> Rules a -> Rules a
-modifyRules f (Rules r) = Rules $ do
+modifyRulesScoped :: (SRules -> SRules) -> Rules a -> Rules a
+modifyRulesScoped f (Rules r) = Rules $ do
     (opts, refOld) <- ask
     liftIO $ do
         refNew <- newIORef mempty
@@ -227,7 +227,7 @@
 -- 'priority' p1 (r1 >> r2) === 'priority' p1 r1 >> 'priority' p1 r2
 -- @
 priority :: Double -> Rules a -> Rules a
-priority d = modifyRules $ \s -> s{userRules = TMap.map (\(UserRuleVersioned b x) -> UserRuleVersioned b $ Priority d x) $ userRules s}
+priority d = modifyRulesScoped $ \s -> s{userRules = TMap.map (\(UserRuleVersioned b x) -> UserRuleVersioned b $ Priority d x) $ userRules s}
 
 
 -- | Indicate that the nested rules have a given version. If you change the semantics of the rule then updating (or adding)
@@ -241,7 +241,7 @@
 --   You should only use 'versioned' to track changes in the build source, for standard runtime dependencies you should use
 --   other mechanisms, e.g. 'Development.Shake.addOracle'.
 versioned :: Int -> Rules a -> Rules a
-versioned v = modifyRules $ \s -> s
+versioned v = modifyRulesScoped $ \s -> s
     {userRules = TMap.map (\(UserRuleVersioned b x) -> UserRuleVersioned (b || v /= 0) $ Versioned (Ver v) x) $ userRules s
     ,builtinRules = Map.map (\b -> b{builtinVersion = Ver v}) $ builtinRules s
     }
@@ -260,7 +260,7 @@
 --   Inside 'alternatives' the 'priority' of each rule is not used to determine which rule matches,
 --   but the resulting match uses that priority compared to the rules outside the 'alternatives' block.
 alternatives :: Rules a -> Rules a
-alternatives = modifyRules $ \r -> r{userRules = TMap.map (\(UserRuleVersioned b x) -> UserRuleVersioned b $ Alternative x) $ userRules r}
+alternatives = modifyRulesScoped $ \r -> r{userRules = TMap.map (\(UserRuleVersioned b x) -> UserRuleVersioned b $ Alternative x) $ userRules r}
 
 
 -- | Run an action, usually used for specifying top-level requirements.
@@ -287,4 +287,4 @@
 -- | Remove all actions specified in a set of rules, usually used for implementing
 --   command line specification of what to build.
 withoutActions :: Rules a -> Rules a
-withoutActions = modifyRules $ \x -> x{actions=mempty}
+withoutActions = modifyRulesScoped $ \x -> x{actions=mempty}
diff --git a/src/Development/Shake/Internal/Core/Run.hs b/src/Development/Shake/Internal/Core/Run.hs
--- a/src/Development/Shake/Internal/Core/Run.hs
+++ b/src/Development/Shake/Internal/Core/Run.hs
@@ -73,6 +73,12 @@
 open cleanup opts rs = withInit opts $ \opts@ShakeOptions{..} diagnostic _ -> do
     diagnostic $ return "Starting run"
     (actions, ruleinfo, userRules) <- runRules opts rs
+
+    diagnostic $ return $ "Number of actions = " ++ show (length actions)
+    diagnostic $ return $ "Number of builtin rules = " ++ show (Map.size ruleinfo) ++ " " ++ show (Map.keys ruleinfo)
+    diagnostic $ return $ "Number of user rule types = " ++ show (TMap.size userRules)
+    diagnostic $ return $ "Number of user rules = " ++ show (sum (TMap.toList (userRuleSize . userRuleContents) userRules))
+
     checkShakeExtra shakeExtra
     curdir <- getCurrentDirectory
 
diff --git a/src/Development/Shake/Internal/Core/Types.hs b/src/Development/Shake/Internal/Core/Types.hs
--- a/src/Development/Shake/Internal/Core/Types.hs
+++ b/src/Development/Shake/Internal/Core/Types.hs
@@ -5,7 +5,7 @@
 module Development.Shake.Internal.Core.Types(
     BuiltinRun, BuiltinLint, BuiltinIdentity,
     RunMode(..), RunResult(..), RunChanged(..),
-    UserRule(..), UserRuleVersioned(..),
+    UserRule(..), UserRuleVersioned(..), userRuleSize,
     BuiltinRule(..), Global(..), Local(..), Action(..), runAction, addDiscount,
     newLocal, localClearMutable, localMergeMutable,
     Stack, Step(..), Result(..), Database(..), Depends(..), Status(..), Trace(..), BS_Store,
@@ -364,14 +364,18 @@
     mappend = (<>)
 
 instance Semigroup (UserRule a) where
-    x <> y = Unordered $ fromUnordered x ++ fromUnordered y
-        where
-            fromUnordered (Unordered xs) = xs
-            fromUnordered x = [x]
+    x <> y = Unordered [x,y]
 
 instance Monoid (UserRule a) where
     mempty = Unordered []
     mappend = (<>)
+
+userRuleSize :: UserRule a -> Int
+userRuleSize UserRule{} = 1
+userRuleSize (Unordered xs) = sum $ map userRuleSize xs
+userRuleSize (Priority _ x) = userRuleSize x
+userRuleSize (Alternative x) = userRuleSize x
+userRuleSize (Versioned _ x) = userRuleSize x
 
 
 -- | Invariant: The database does not have any cycles where a Key depends on itself.
diff --git a/src/General/TypeMap.hs b/src/General/TypeMap.hs
--- a/src/General/TypeMap.hs
+++ b/src/General/TypeMap.hs
@@ -1,17 +1,18 @@
 {-# LANGUAGE ExistentialQuantification, ConstraintKinds, KindSignatures, GADTs, ScopedTypeVariables, Rank2Types #-}
 
 module General.TypeMap(
-    Map, empty, singleton, insert, map, lookup, unionWith
+    Map, empty, singleton, insert, map, lookup, unionWith, toList, size
     ) where
 
 import qualified Data.HashMap.Strict as Map
 import Data.Typeable.Extra
 import Unsafe.Coerce
 import Data.Functor
+import qualified Prelude
 import Prelude hiding (lookup, map)
 
 
-data F f = forall a . F (f a)
+data F f = forall a . F !(f a)
 
 unF :: F f -> f a
 unF x = case x of F x -> unsafeCoerce x
@@ -35,3 +36,9 @@
 
 map :: (forall a . f1 a -> f2 a) -> Map f1 -> Map f2
 map f (Map mp) = Map $ Map.map (\(F a) -> F $ f a) mp
+
+toList :: (forall a . f a -> b) -> Map f -> [b]
+toList f (Map mp) = Prelude.map (\(F a) -> f a) $ Map.elems mp
+
+size :: Map f -> Int
+size (Map mp) = Map.size mp
